Showing preview only (834K chars total). Download the full file or copy to clipboard to get everything.
Repository: Drazzilb08/daps
Branch: master
Commit: 9ea4bdffa389
Files: 96
Total size: 796.9 KB
Directory structure:
gitextract_jvbd20pz/
├── .dockerignore
├── .github/
│ └── workflows/
│ ├── on-branch-create.yml
│ ├── on-branch-delete.yml
│ ├── on-commit.yml
│ └── version.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── VERSION
├── compose/
│ └── docker-compose.yml
├── jokes.txt
├── main.py
├── modules/
│ ├── __init__.py
│ ├── border_replacerr.py
│ ├── health_checkarr.py
│ ├── jduparr.py
│ ├── labelarr.py
│ ├── nohl.py
│ ├── poster_cleanarr.py
│ ├── poster_renamerr.py
│ ├── renameinatorr.py
│ ├── sync_gdrive.py
│ ├── unmatched_assets.py
│ └── upgradinatorr.py
├── requirements.txt
├── start.sh
├── util/
│ ├── __init__.py
│ ├── arrpy.py
│ ├── assets.py
│ ├── config.py
│ ├── constants.py
│ ├── construct.py
│ ├── extract.py
│ ├── index.py
│ ├── logger.py
│ ├── match.py
│ ├── normalization.py
│ ├── notification.py
│ ├── notification_formatting.py
│ ├── scanner.py
│ ├── scheduler.py
│ ├── template/
│ │ └── config_template.json
│ ├── utility.py
│ └── version.py
└── web/
├── server.py
├── static/
│ ├── css/
│ │ ├── base.css
│ │ ├── common.css
│ │ ├── index.css
│ │ ├── instances.css
│ │ ├── layout.css
│ │ ├── logs.css
│ │ ├── modals.css
│ │ ├── navigation.css
│ │ ├── notifications.css
│ │ ├── poster_search.css
│ │ ├── schedule.css
│ │ └── settings.css
│ └── js/
│ ├── common.js
│ ├── help_content.js
│ ├── helper.js
│ ├── index.js
│ ├── instances.js
│ ├── logs.js
│ ├── main.js
│ ├── navigation.js
│ ├── notifications.js
│ ├── payload.js
│ ├── poster_search.js
│ ├── schedule.js
│ ├── settings/
│ │ ├── constants.js
│ │ ├── modal_helpers.js
│ │ ├── modals.js
│ │ ├── modules/
│ │ │ ├── border_replacerr.js
│ │ │ ├── health_checkarr.js
│ │ │ ├── jduparr.js
│ │ │ ├── labelarr.js
│ │ │ ├── main.js
│ │ │ ├── nohl.js
│ │ │ ├── poster_cleanarr.js
│ │ │ ├── poster_renamerr.js
│ │ │ ├── renameinatorr.js
│ │ │ ├── sync_gdrive.js
│ │ │ ├── unmatched_assets.js
│ │ │ └── upgradinatorr.js
│ │ ├── presets.js
│ │ └── settings_helpers.js
│ └── settings.js
└── templates/
├── index.html
└── pages/
├── instances.html
├── logs.html
├── notifications.html
├── poster_search.html
├── schedule.html
└── settings.html
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# Ignore everything
*
# Allow files and directories
!/main.py
!/requirements.txt
!/jokes.txt
!/modules
!/util
!/scripts
!/config
!/VERSION
!/spud
!/start.sh
!/web
# Ignore unnecessary files inside allowed directories
# This should go after the allowed directories
**/*~
**/*.log
**/.DS_Store
**/Thumbs.db
**/config.yml
**/Dockerfile
================================================
FILE: .github/workflows/on-branch-create.yml
================================================
name: Tag Docker Image for New Branch
on:
create:
branches:
- '*' # Triggers on branch creation
jobs:
docker-tag:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Get the new branch name
id: get_branch
run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
- name: Set build number
run: echo "BUILD_NUMBER=$(git rev-list --count HEAD)" >> $GITHUB_ENV
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GH_USERNAME }}
password: ${{ secrets.GH_TOKEN }}
- name: Build and push Docker image to GHCR
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
build-args: |
"BRANCH=${{ steps.get_branch.outputs.BRANCH_NAME }}"
"BUILD_NUMBER=${{ env.BUILD_NUMBER }}"
push: true
tags: |
ghcr.io/drazzilb08/daps:${{ steps.get_branch.outputs.BRANCH_NAME }}
================================================
FILE: .github/workflows/on-branch-delete.yml
================================================
name: Delete GHCR Docker Tag for Deleted Branch
on:
delete:
branches:
- '*' # Triggers on branch deletion
jobs:
ghcr-delete-tag:
runs-on: ubuntu-latest
steps:
- name: Get the deleted branch name
id: get_branch
run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
- name: Check if branch is not dev or master
run: |
if [[ "${{ steps.get_branch.outputs.BRANCH_NAME }}" == "dev" || "${{ steps.get_branch.outputs.BRANCH_NAME }}" == "master" ]]; then
echo "Skipping deletion for branch: ${{ steps.get_branch.outputs.BRANCH_NAME }}"
exit 0
fi
- name: Delete tag from GHCR
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
TAG_NAME: ${{ steps.get_branch.outputs.BRANCH_NAME }}
run: |
REPO="drazzilb08/daps"
# Get the tag digest
DIGEST=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \
"https://ghcr.io/v2/${REPO}/manifests/${TAG_NAME}" \
-I | grep -i 'docker-content-digest:' | awk '{print $2}' | tr -d '\r')
if [[ -z "$DIGEST" ]]; then
echo "Tag not found on GHCR"
exit 0
fi
# Delete the tag
curl -s -X DELETE -H "Authorization: Bearer $GH_TOKEN" \
"https://ghcr.io/v2/${REPO}/manifests/${DIGEST}" \
&& echo "Deleted GHCR tag: ${TAG_NAME}" || echo "Failed to delete GHCR tag: ${TAG_NAME}"
================================================
FILE: .github/workflows/on-commit.yml
================================================
name: Update Docker Image on Branch Commit
on:
push:
branches:
- '*' # Triggers on push to any branch
paths:
- 'Dockerfile'
- 'main.py'
- 'requirements.txt'
- 'jokes.txt'
- 'modules/**'
- 'util/**'
- 'scripts/**'
- 'config/**'
- 'spud/**'
- 'web/**'
- 'VERSION'
- 'start.sh'
jobs:
docker-tag:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Get the current branch name
id: get_branch
run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
- name: Set build number
run: echo "BUILD_NUMBER=$(git rev-list --count HEAD)" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GH_USERNAME }}
password: ${{ secrets.GH_TOKEN }}
- name: Build and push Docker image (branch-specific)
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
build-args: |
"BRANCH=${{ steps.get_branch.outputs.BRANCH_NAME }}"
"BUILD_NUMBER=${{ env.BUILD_NUMBER }}"
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/daps:${{ steps.get_branch.outputs.BRANCH_NAME }}
ghcr.io/drazzilb08/daps:${{ steps.get_branch.outputs.BRANCH_NAME }}
- name: Build and push Docker image (latest tag for master)
if: ${{ steps.get_branch.outputs.BRANCH_NAME == 'master' }}
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
build-args: |
"BRANCH=master"
"BUILD_NUMBER=${{ env.BUILD_NUMBER }}"
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/daps:latest
ghcr.io/drazzilb08/daps:latest
================================================
FILE: .github/workflows/version.yml
================================================
name: Docker Version Release
on:
push:
tags:
- v*
jobs:
docker-version:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Get the version
id: get_version
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
id: extract_branch
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GH_USERNAME }}
password: ${{ secrets.GH_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
build-args: |
"BRANCH=${{ steps.extract_branch.outputs.branch }}"
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/daps:${{ steps.get_version.outputs.VERSION }}
ghcr.io/drazzilb08/daps:${{ steps.get_version.outputs.VERSION }}
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
dist/
*.egg-info/
*.egg
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Virtual environments
venv/
env/
ENV/
.venv/
# Bash
*.sh~
# IDEs / editors
.vscode/
.idea/
*.swp
*.swo
*.swn
*.bak
# Ignore Directories
.archives/
.extra_scripts/
screenshots/
logs/
tests/
# Ignore Files
**/config.yml
**/.DS_Store
**/TODO.*
**test.**
**side-projects**
test2.py
.env
/tests
pyproject.toml
pyproject.toml
/config
================================================
FILE: Dockerfile
================================================
# Single-stage build for installing Python dependencies and required packages
FROM python:3.11-slim
# Copy requirements.txt and install Python dependencies
COPY requirements.txt .
# Install required packages and Python dependencies
RUN set -eux; \
apt-get update && \
apt-get install -y --no-install-recommends \
gcc wget curl unzip p7zip-full tzdata jq git build-essential && \
pip3 install --no-cache-dir -r requirements.txt && \
curl https://rclone.org/install.sh | bash && \
git clone https://codeberg.org/jbruchon/libjodycode.git /tmp/libjodycode && \
make -C /tmp/libjodycode && make -C /tmp/libjodycode install && \
ldconfig && \
git clone https://codeberg.org/jbruchon/jdupes.git /tmp/jdupes && \
make -C /tmp/jdupes && make -C /tmp/jdupes install && \
ln -s /usr/local/bin/jdupes /usr/bin/jdupes && \
rm -rf /tmp/libjodycode /tmp/jdupes
# Clean up
RUN set -eux; \
apt-get remove -y --purge gcc build-essential && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Metadata and labels
LABEL maintainer="Drazzilb" \
description="daps" \
org.opencontainers.image.source="https://github.com/Drazzilb08/daps" \
org.opencontainers.image.authors="Drazzilb" \
org.opencontainers.image.title="daps"
# Branch and build number arguments
ARG BRANCH="master"
ARG BUILD_NUMBER=""
# Pass the build-time BRANCH arg into a runtime environment variable
ENV BRANCH=${BRANCH}
ENV BUILD_NUMBER=${BUILD_NUMBER}
ARG CONFIG_DIR=/config
# Set script environment variables
ENV CONFIG_DIR=/config
ENV APPDATA_PATH=/appdata
ENV LOG_DIR=/config/logs
ENV TZ=America/Los_Angeles
ENV PORT=8000
ENV HOST=0.0.0.0
ENV DOCKER_ENV=true
# Expose the application port
EXPOSE ${PORT}
VOLUME /config
WORKDIR /app
COPY . .
# Create a new user called dockeruser with the specified PUID and PGID
RUN groupadd -g 99 dockeruser; \
useradd -u 100 -g 99 dockeruser; \
chown -R dockeruser:dockeruser /app;
# Entrypoint script
CMD ["bash", "start.sh"]
================================================
FILE: LICENSE
================================================
piMIT License
Copyright (c) 2023 Drazzilb
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
# Create venv if it doesn't exist
.PHONY: venv
venv:
test -d venv || python3 -m venv venv
# Install requirements
.PHONY: install
install: venv
. venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt
# Freeze current venv into requirements.txt
.PHONY: lock
lock:
. venv/bin/activate && pip freeze > requirements.txt
# Lint using flake8 (must be installed in requirements.txt)
.PHONY: lint
lint:
. venv/bin/activate && flake8
================================================
FILE: README.md
================================================
<div align="center">
# DAPS
Automate, optimize, and take control of your media libraries.
[](https://opensource.org/licenses/MIT)
[](https://github.com/Drazzilb08/daps/issues)
[](https://github.com/Drazzilb08/daps/pulls)
[](https://github.com/Drazzilb08/daps/stargazers)
[](https://www.python.org/)
[](https://www.gnu.org/software/bash/)
</div>
---
## 🚀 Quickstart
See [Wiki](https://github.com/Drazzilb08/daps/wiki) for full install docs.
**Docker (recommended):**
```bash
docker run -d -v /path/to/config:/config -v /path/to/posters:/posters -p 8000:8000 drazzilb08/daps
```
**Local:**
```bash
git clone https://github.com/Drazzilb08/daps.git
cd daps
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python3 main.py poster_renamerr
```
and use the built-in Web UI: [http://localhost:8000](http://localhost:8000)
---
## 🙋♂️ Contributing & Support
Pull requests are welcome for fixes, docs, or new module ideas.
If you spot a bug or want a feature, open an [Issue](https://github.com/Drazzilb08/daps/issues) or jump into a PR.
---
<div align="center">
Made with ❤️ by Drazzilb
If this saved you time, star the repo, tell a friend, or buy yourself a cookie.
</div>
================================================
FILE: VERSION
================================================
2.0.3
================================================
FILE: compose/docker-compose.yml
================================================
version: "3.9"
services:
daps:
container_name: daps
image: ghcr.io/drazzilb08/daps:latest
ports:
- "8000:8000"
volumes:
- /path/to/config:/config
- /path/to/kometa/assets/:/kometa
- /path/to/posters:/posters
- /path/to/media:/media
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TIMEZONE}
restart: unless-stopped
================================================
FILE: jokes.txt
================================================
I'm reading a book about anti-gravity. It's impossible to put down.
I burned 2000 calories today I left my food in the oven for too long.
I startled my next-door neighbor with my new electric power tool. I had to calm him down by saying “Don’t worry, this is just a drill!”
I broke my arm in two places. My doctor told me to stop going to those places.
I quit my job at the coffee shop the other day. It was just the same old grind over and over.
I never buy anything that has Velcro with it... it’s a total rip-off.
I used to work at a soft drink can crushing company... it was soda pressing.
I wondered why the frisbee kept on getting bigger. Then it hit me.
I was going to tell you a fighting joke... but I forgot the punch line.
What is the most groundbreaking invention of all time? The shovel.
I’m starting my new job at a restaurant next week. I can’t wait.
I visited a weight loss website... they told me I have to have cookies disabled.
Did you hear about the famous Italian chef that recently died? He pasta way.
Broken guitar for sale no strings attached.
I could never be a plumber it’s too hard watching your life’s work go down the drain.
I cut my finger slicing cheese the other day... but I think I may have grater problems than that.
What time did you go to the dentist yesterday? Tooth-hurty.
What kind of music do astronauts listen to? Neptunes.
Rest in peace, boiled water. You will be mist.
What is the only concert in the world that costs 45 cents? 50 Cent, featuring Nickelback.
It’s not a dad bod it’s a father figure.
My wife recently went on a tropical food diet and now our house is full of this stuff. It’s enough to make a mango crazy.
What do you call Santa’s little helpers? Subordinate clauses.
Want to hear a construction joke? Sorry, I’m still working on it.
What’s the difference between a hippo and a zippo? One is extremely big and heavy, and the other is a little lighter.
I burnt my Hawaiian pizza today in the oven, I should have cooked it on aloha temperature.
Anyone can be buried when they die but if you want to be cremated then you have to urn it.
Where did Captain Hook get his hook? From the second-hand store.
I am such a good singer that people always ask me to sing solo solo that they can’t hear me.
I am such a good singer that people ask me to sing tenor tenor twelve miles away.
Occasionally to relax I just like to tuck my knees into my chest and lean forward. That’s just how I roll.
What did the glass of wine say to the glass of beer? Nothing. They barley knew each other.
I’ve never trusted stairs. They are always up to something.
Why did Shakespeare’s wife leave him? She got sick of all the drama.
I just bought a dictionary but all of the pages are blank. I have no words to describe how mad I am.
If you want to get a job at the moisturizer factory... you’re going to have to apply daily.
I don’t know what’s going to happen next year. It’s probably because I don’t have 2020 vision.
Want to hear a joke about going to the bathroom? Urine for a treat.
I couldn’t figure out how to use the seat belt. Then it just clicked.
I got an email the other day teaching me how to read maps backwards turns out it was just spam.
I'm reading a book about anti-gravity. It's impossible to put down!
You're American when you go into the bathroom, and you're American when you come out, but do you know what you are while you're in there? European.
Did you know the first French fries weren't actually cooked in France? They were cooked in Greece.
Want to hear a joke about a piece of paper? Never mind... it's tearable.
I just watched a documentary about beavers. It was the best dam show I ever saw!
If you see a robbery at an Apple Store what re you? An iWitness?
Spring is here! I got so excited I wet my plants!
Why did the Clydesdale give the pony a glass of water? Because he was a little horse!
CASHIER: "Would you like the milk in a bag, sir?" DAD: "No, just leave it in the carton!’”
Did you hear about the guy who invented Lifesavers? They say he made a mint.
I bought some shoes from a drug dealer. I don't know what he laced them with, but I was tripping all day!
Why do chicken coops only have two doors? Because if they had four, they would be chicken sedans!
How do you make a Kleenex dance? Put a little boogie in it!
Why did the invisible man turn down the job offer? He couldn't see himself doing it.
I used to have a job at a calendar factory but I got the sack because I took a couple of days off.
A woman is on trial for beating her husband to death with his guitar collection. Judge says, "First offender?" She says, "No, first a Gibson! Then a Fender!”
I had a dream that I was a muffler last night. I woke up exhausted!
Did you hear about the circus fire? It was in tents!
Don't trust atoms. They make up everything!
How many tickles does it take to make an octopus laugh? Ten-tickles.
I’m only familiar with 25 letters in the English language. I don’t know why.
Why did the cow in the pasture get promoted at work? Because he is OUT-STANDING in his field!
What do prisoners use to call each other? Cell phones.
Why couldn't the bike standup by itself? It was two tired.
Who was the fattest knight at King Arthur’s round table? Sir Cumference.
Did you see they made round bails of hay illegal in Wisconsin? It’s because the cows weren’t getting a square meal.
You know what the loudest pet you can get is? A trumpet.
What do you get when you cross a snowman with a vampire? Frostbite.
What do you call a deer with no eyes? No idea!
Can February March? No, but April May!
What do you call a lonely cheese? Provolone.
Why can't you hear a pterodactyl go to the bathroom? Because the pee is silent.
What did the buffalo say to his son when he dropped him off at school? Bison.
What do you call someone with no body and no nose? Nobody knows.
You heard of that new band 1023MB? They're good but they haven't got a gig yet.
Why did the crab never share? Because he's shellfish.
How do you get a squirrel to like you? Act like a nut.
Why don't eggs tell jokes? They'd crack each other up.
Why can't a nose be 12 inches long? Because then it would be a foot.
Did you hear the rumor about butter? Well, I'm not going to spread it!
I made a pencil with two erasers. It was pointless.
I used to hate facial hair... but then it grew on me.
I decided to sell my vacuum cleaner— it was just gathering dust!
I had a neck brace fitted years ago and I've never looked back since.
You know, people say they pick their nose, but I feel like I was just born with mine.
What do you call an elephant that doesn't matter? An irrelephant.
What do you get from a pampered cow? Spoiled milk.
It's inappropriate to make a 'dad joke' if you're not a dad. It's a faux pa.
How do lawyers say goodbye? Sue ya later!
Wanna hear a joke about paper? Never mind—it's tearable.
What's the best way to watch a fly fishing tournament? Live stream.
I could tell a joke about pizza, but it's a little cheesy.
When does a joke become a dad joke? When it becomes apparent.
What’s an astronaut’s favorite part of a computer? The space bar.
What did the shy pebble wish for? That she was a little boulder.
Why didn’t the skeleton cross the road? Because he had no guts.
What did one nut say as he chased another nut? I'm a cashew!
Chances are if you' ve seen one shopping center... you've seen a mall.
I knew I shouldn't steal a mixer from work... but it was a whisk I was willing to take.
How come the stadium got hot after the game? Because all of the fans left.
Why was it called the dark ages? Because of all the knights.
Why did the tomato blush? Because it saw the salad dressing.
Did you hear the joke about the wandering nun? She was a roman catholic.
What creature is smarter than a talking parrot? A spelling bee.
I'll tell you what often gets over looked... garden fences.
Why did the kid cross the playground? To get to the other slide.
Why do birds fly south for the winter? Because it's too far to walk.
What is a centipedes's favorite Beatle song? I want to hold your hand, hand, hand, hand...
My first time using an elevator was an uplifting experience. The second time let me down.
To be Frank... I'd have to change my name.
Slept like a log last night … woke up in the fireplace.
How many South Americans does it take to change a lightbulb? A Brazilian
What is the difference between ignorance and apathy? I don't know and I don't care.
I went to a Foo Fighters Concert once... It was Everlong...
Some people eat light bulbs. They say it's a nice light snack.
What do you get hanging from Apple trees? Sore arms.
What did Romans use to cut pizza before the rolling cutter was invented? Lil Caesars
My pet mouse 'Elvis' died last night. He was caught in a trap..
Never take advice from electrons. They are always negative.
Why are oranges the smartest fruit? Because they are made to concentrate.
What did the beaver say to the tree? It's been nice gnawing you.
How do you fix a damaged jack-o-lantern? You use a pumpkin patch.
What did the late tomato say to the early tomato? I’ll ketch up
I have kleptomania... when it gets bad, I take something for it.
I used to be addicted to soap... but I'm clean now.
When is a door not a door? When it's ajar.
I made a belt out of watches once... It was a waist of time.
This furniture store keeps emailing me, all I wanted was one night stand!
How do you find Will Smith in the snow? Look for fresh prints.
If at first you don't succeed sky diving is not for you!
What kind of music do mummy's like? Rap
A book just fell on my head. I only have my shelf to blame.
What did the dog say to the two trees? Bark bark.
If a child refuses to sleep during nap time... are they guilty of resisting a rest?
Why should you never trust a pig with a secret? Because it's bound to squeal.
Why are mummys scared of vacation? They're afraid to unwind.
Whiteboards ... are remarkable.
What kind of dinosaur loves to sleep? A stega-snore-us.
Why don't scientists trust atoms? Because they make up everything.
What do you call a dinosaur that is sleeping? A dino-snore.
What do you call a dinosaur that never gives up? Try and try and try and try-ceratops.
What kind of tree fits in your hand? A palm tree!
I used to be addicted to the hokey pokey but I turned myself around.
What do you call a fake noodle? An impasta.
What do you call a cow with two legs? Lean beef.
How many tickles does it take to tickle an octopus? Ten-tickles!
What musical instrument is found in the bathroom? A tuba toothpaste.
My boss told me to attach two pieces of wood together... I totally nailed it!
What was the pumpkin’s favorite sport? Squash.
What do you call corn that joins the army? Kernel.
I've been trying to come up with a dad joke about momentum but I just can't seem to get it going.
Why don't sharks eat clowns? Because they taste funny.
Why didn’t the melons get married? Because they cantaloupe.
What’s a computer’s favorite snack? Microchips!
Why was the robot so tired after his road trip? He had a hard drive.
Why did the computer have no money left? Someone cleaned out its cache!
I'm not anti-social. I'm just not user friendly.
Why did the computer get cold? Because it forgot to close windows.
What is an astronaut's favorite key on a keyboard? The space bar!
What's the difference between a computer salesman and a used-car salesman? The used-car salesman KNOWS when he's lying.
If at first you don't succeed... call it version 1.0
Why did Microsoft PowerPoint cross the road? To get to the other slide!
What did the computer do at lunchtime? Had a byte!
Why did the computer keep sneezing? It had a virus!
What did one toilet say to the other? You look a bit flushed.
Why did the picture go to jail? Because it was framed.
What did one wall say to the other wall? I'll meet you at the corner.
What do you call a boy named Lee that no one talks to? Lonely
Why do bicycles fall over? Because they are two-tired!
Why was the broom late? It over swept!
What part of the car is the laziest? The wheels, because they are always tired!
What's the difference between a TV and a newspaper? Ever tried swatting a fly with a TV?
What did one elevator say to the other elevator? I think I'm coming down with something!
Why was the belt arrested? Because it held up some pants!
What makes the calendar seem so popular? Because it has a lot of dates!
Why do you go to bed every night? Because the bed won't come to you!
What has four wheels and flies? A garbage truck!
Why did the robber take a bath before he stole from the bank? He wanted to make a clean get away!
Just watched a documentary about beavers. It was the best damn program I’ve ever seen.
Slept like a log last night woke up in the fireplace.
What’s the difference between an African elephant and an Indian elephant? About 5000 miles
Why did the coffee file a police report? It got mugged.
What did the grape do when he got stepped on? He let out a little wine.
How many apples grow on a tree? All of them.
What name do you give a person with a rubber toe? Roberto
Why do scuba divers fall backwards into the water? Because if they fell forwards they’d still be in the boat.
How does a penguin build it’s house? Igloos it together.
What do you call a man with a rubber toe? Roberto
Did you hear about the restaurant on the moon? Great food, no atmosphere.
Why was the belt sent to jail? For holding up a pair of pants!
Did you hear about the scientist who was lab partners with a pot of boiling water? He had a very esteemed colleague.
What happens when a frogs car dies? He needs a jump. If that doesn't work he has to get it toad.
What did the flowers do when the bride walked down the aisle? They rose.
Why did the man fall down the well? Because he couldn’t see that well.
My boss told me to have a good day... ...so I went home.
How can you tell it’s a dogwood tree? By the bark.
Did you hear about the kidnapping at school? It’s fine, he woke up.
Why is Peter Pan always flying? Because he Neverlands.
Which state has the most streets? Rhode Island.
What do you call 26 letters that went for a swim? Alphawetical.
Why was the color green notoriously single? It was always so jaded.
Why did the coach go to the bank? To get his quarterback.
How do celebrities stay cool? They have many fans.
What's the most depressing day of the week? sadder day.
Dogs can’t operate MRI machines But catscan.
I was going to tell a time-traveling joke but you guys didn’t like it.
Stop looking for the perfect match instead look for a lighter.
I told my doctor I heard buzzing but he said it’s just a bug going around.
What kind of car does a sheep like to drive? A lamborghini.
What did the accountant say while auditing a document? This is taxing.
What did the two pieces of bread say on their wedding day? It was loaf at first sight.
Why do melons have weddings? Because they cantaloupe.
What did the drummer call his twin daughters? Anna One, Anna Two!
What do you call a toothless bear? A gummy bear!
Two goldfish are in a tank. One says to the other, “Do you know how to drive this thing?”
What’s Forrest Gump’s password? 1forrest1
What is a child guilty of if they refuse to nap? Resisting a rest.
I know a lot of jokes about retired people but none of them work.
Why are spiders so smart? They can find everything on the web.
What has one head, one foot, and four legs? A bed.
What does a house wear? Address.
What’s red and smells like blue paint? Red paint.
My son asked me to put his shoes on but I don’t think they’ll fit me.
I’ve been bored recently, so I decided to take up fencing. The neighbors keep demanding that I put it back.
What do you call an unpredictable camera? A loose Canon.
Which U.S. state is known for its especially small soft drinks? Minnesota.
What do sprinters eat before a race? Nothing—they fast.
I’m so good at sleeping... I can do it with my eyes closed.
People are usually shocked that I have a Police record. But I love their greatest hits!
I told my girlfriend she drew on her eyebrows too high. She seemed surprised.
What do you call a fibbing cat? A lion.
Why shouldn’t you write with a broken pencil? Because it’s pointless.
I like telling Dad jokes… sometimes he laughs.
How do you weigh a millennial? In Instagrams.
The wedding was so beautiful even the cake was in tiers.
What’s the most patriotic sport? Flag football.
Never trust atoms; they make up everything.
Two fish are in a tank. One says, How do you drive this thing?
My wife told me to stop impersonating a flamingo. I had to put my foot down.
I went to buy some camo pants but couldn’t find any.
I failed math so many times at school, I can’t even count.
I used to have a handle on life, but then it broke.
I was wondering why the frisbee kept getting bigger and bigger, but then it hit me.
I heard there were a bunch of break-ins over at the car park. That is wrong on so many levels.
I want to die peacefully in my sleep, like my grandfather… Not screaming and yelling like the passengers in his car.
When life gives you melons, you might be dyslexic
Don’t you hate it when someone answers their own questions? I do.
It takes a lot of balls to golf the way I do.
I told him to be himself; that was pretty mean, I guess.
I know they say that money talks, but all mine says is ‘Goodbye.’
My father has schizophrenia, but he’s good people.
The problem with kleptomaniacs is that they always take things literally.
I can’t believe I got fired from the calendar factory. All I did was take a day off.
Most people are shocked when they find out how bad I am as an electrician.
Never trust atoms; they make up everything.
My wife just found out I replaced our bed with a trampoline. She hit the ceiling!
I was addicted to the hokey pokey, but then I turned myself around.
I used to think I was indecisive. But now I’m not so sure.
Russian dolls are so full of themselves.
The easiest time to add insult to injury is when you’re signing someone’s cast.
Light travels faster than sound, which is the reason that some people appear bright before you hear them speak.
My therapist says I have a preoccupation for revenge. We’ll see about that.
A termite walks into the bar and asks, ‘Is the bar tender here?’
A told my girlfriend she drew her eyebrows too high. She seemed surprised.
People who use selfie sticks really need to have a good, long look at themselves.
Two fish are in a tank. One says, ‘How do you drive this thing?’
I always take life with a grain of salt. And a slice of lemon. And a shot of tequila.
Just burned 2,000 calories. That’s the last time I leave brownies in the oven while I nap.
Always borrow money from a pessimist. They’ll never expect it back.
Build a man a fire and he’ll be warm for a day. Set a man on fire and he’ll be warm for the rest of his life.
I don’t suffer from insanity—I enjoy every minute of it.
The last thing I want to do is hurt you; but it’s still on the list.
The problem isn’t that obesity runs in your family. It’s that no one runs in your family.
Today a man knocked on my door and asked for a small donation toward the local swimming pool. I gave him a glass of water.
I’m reading a book about anti-gravity. It’s impossible to put down.
‘Doctor, there’s a patient on line one that says he’s invisible.’ ‘Well, tell him I can’t see him right now.’
Atheism is a non-prophet organization.
A recent study has found that women who carry a little extra weight live longer than the men who mention it.
The future, the present, and the past walk into a bar. Things got a little tense.
Before you criticize someone, walk a mile in their shoes. That way, when you do criticize them, you’re a mile away and you have their shoes.
Last night my girlfriend was complaining that I never listen to her… or something like that.
Maybe if we start telling people their brain is an app, they’ll want to use it.
If a parsley farmer gets sued, can they garnish his wages?
I got a new pair of gloves today, but they’re both ‘lefts,’ which on the one hand is great, but on the other, it’s just not right.
I didn’t think orthopedic shoes would help, but I stand corrected.
I was riding a donkey the other day when someone threw a rock at me and I fell off. I guess I was stoned off my ass.
People who take care of chickens are literally chicken tenders.
It was an emotional wedding. Even the cake was in tiers.
I just got kicked out of a secret cooking society. I spilled the beans.
What’s a frog’s favorite type of shoes? Open toad sandals.
Blunt pencils are really pointless.
6:30 is the best time on a clock, hands down.
Two wifi engineers got married. The reception was fantastic.
Just got fired from my job as a set designer. I left without making a scene.
What’s the difference between ignorance and apathy? I don’t know and I don’t care.
I buy all my guns from a guy called T-Rex. He's a small arms dealer.
One of the cows didn’t produce milk today. It was an udder failure.
Adam & Eve were the first ones to ignore the Apple terms and conditions.
Refusing to go to the gym is a form of resistance training.
If attacked by a mob of clowns, go for the juggler.
The man who invented Velcro has died. RIP.
Despite the high cost of living, it remains popular.
A dung beetle walks into a bar and asks, ‘Is this stool taken?’
I can tell when people are being judgmental just by looking at them.
The rotation of Earth really makes my day.
Well, to be Frank with you, I’d have to change my name.
My friend was explaining electricity to me, but I was like, ‘Watt?’
What if there were no hypothetical questions?
Are people born with photographic memories, or does it take time to develop?
The world champion tongue twister got arrested. I hear they’re going to give him a tough sentence.
Pollen is what happens when flowers can’t keep it in their plants.
A book fell on my head the other day. I only have my shelf to blame though.
Communist jokes aren’t funny unless everyone gets them.
Geology rocks, but geography’s where it’s at.
I buy all my guns from a guy called T-Rex. He’s a small arms dealer.
My friend’s bakery burned down last night. Now his business is toast.
Four fonts walk into a bar. The bartender says, ‘Hey! We don’t want your type in here!’
If you don’t pay your exorcist, do you get repossessed?
When the cannibal showed up late to the buffet, they gave him the cold shoulder.
A Mexican magician tells the audience he will disappear on the count of three. He says, ‘Uno, dos…” and poof! He disappeared without a tres.
Fighting for peace is like screwing for virginity.
A ghost walked into a bar and ordered a shot of vodka. The bartender said, ‘Sorry, we don’t serve spirits here.’
The man who invented knock-knock jokes should get a no bell prize.
I bought the world’s worst thesaurus yesterday. Not only is it terrible, it’s also terrible.
A blind man walked into a bar… and a table… and a chair…
A Freudian slip is when you mean one thing and mean your mother.
I went to a seafood disco last week, but ended up pulling a mussel.
The first time I got a universal remote control, I thought to myself, ‘This changes everything.’
How do you make holy water? You boil the hell out of it.
I saw a sign the other day that said, ‘Watch for children,’ and I thought, ‘That sounds like a fair trade.’
Whiteboards are remarkable.
I threw a boomerang a couple years ago; I know live in constant fear.
I put my grandma on speed dial the other day. I call it insta-gram.
I have a few jokes about unemployed people, but none of them work.
‘I have a split personality,’ said Tom, being Frank.
My teachers told me I'd never amount to much because I procrastinate so much. I told them, "Just you wait!"
Will glass coffins be a success? Remains to be seen.
Did you hear about the guy whose whole left side got amputated? He’s all right now.
The man who survived both mustard gas and pepper spray is a seasoned veteran now.
Have you heard about the new restaurant called ‘Karma?’ There’s no menu—you get what you deserve.
What did one pirate say to the other when he beat him at chess?<>Checkmatey.
I burned 2000 calories today<>I left my food in the oven for too long.
I startled my next-door neighbor with my new electric power tool. <>I had to calm him down by saying “Don’t worry, this is just a drill!”
I broke my arm in two places. <>My doctor told me to stop going to those places.
I quit my job at the coffee shop the other day. <>It was just the same old grind over and over.
I never buy anything that has Velcro with it...<>it’s a total rip-off.
I used to work at a soft drink can crushing company...<>it was soda pressing.
I wondered why the frisbee kept on getting bigger. <>Then it hit me.
I was going to tell you a fighting joke...<>but I forgot the punch line.
What is the most groundbreaking invention of all time? <>The shovel.
I’m starting my new job at a restaurant next week. <>I can’t wait.
I visited a weight loss website...<>they told me I have to have cookies disabled.
Did you hear about the famous Italian chef that recently died? <>He pasta way.
Broken guitar for sale<>no strings attached.
I could never be a plumber<>it’s too hard watching your life’s work go down the drain.
I cut my finger slicing cheese the other day...<>but I think I may have grater problems than that.
What time did you go to the dentist yesterday?<>Tooth-hurty.
What kind of music do astronauts listen to?<>Neptunes.
Rest in peace, boiled water. <>You will be mist.
What is the only concert in the world that costs 45 cents? <>50 Cent, featuring Nickelback.
It’s not a dad bod<> it’s a father figure.
My wife recently went on a tropical food diet and now our house is full of this stuff. <>It’s enough to make a mango crazy.
What do you call Santa’s little helpers? <>Subordinate clauses.
Want to hear a construction joke? <>Sorry, I’m still working on it.
What’s the difference between a hippo and a zippo? <>One is extremely big and heavy, and the other is a little lighter.
I burnt my Hawaiian pizza today in the oven, <>I should have cooked it on aloha temperature.
Anyone can be buried when they die<>but if you want to be cremated then you have to urn it.
Where did Captain Hook get his hook? <>From the second-hand store.
I am such a good singer that people always ask me to sing solo<>solo that they can’t hear me.
I am such a good singer that people ask me to sing tenor<>tenor twelve miles away.
Occasionally to relax I just like to tuck my knees into my chest and lean forward.<> That’s just how I roll.
What did the glass of wine say to the glass of beer? Nothing. <>They barley knew each other.
I’ve never trusted stairs. <>They are always up to something.
Why did Shakespeare’s wife leave him? <>She got sick of all the drama.
I just bought a dictionary but all of the pages are blank. <>I have no words to describe how mad I am.
If you want to get a job at the moisturizer factory... <>you’re going to have to apply daily.
I don’t know what’s going to happen next year. <>It’s probably because I don’t have 2020 vision.
Want to hear a joke about going to the bathroom? <>Urine for a treat.
I couldn’t figure out how to use the seat belt. <>Then it just clicked.
I got an email the other day teaching me how to read maps backwards<>turns out it was just spam.
I'm reading a book about anti-gravity.<> It's impossible to put down!
You're American when you go into the bathroom, and you're American when you come out, but do you know what you are while you're in there?<> European.
Did you know the first French fries weren't actually cooked in France?<> They were cooked in Greece.
Want to hear a joke about a piece of paper? Never mind... <>it's tearable.
I just watched a documentary about beavers. <>It was the best dam show I ever saw!
If you see a robbery at an Apple Store what re you?<> An iWitness?
Spring is here! <>I got so excited I wet my plants!
What’s Forrest Gump’s password?<> 1forrest1
Why did the Clydesdale give the pony a glass of water? <>Because he was a little horse!
CASHIER: "Would you like the milk in a bag, sir?" <>DAD: "No, just leave it in the carton!’”
Did you hear about the guy who invented Lifesavers? <>They say he made a mint.
I bought some shoes from a drug dealer.<> I don't know what he laced them with, but I was tripping all day!
Why do chicken coops only have two doors?<> Because if they had four, they would be chicken sedans!
How do you make a Kleenex dance? <>Put a little boogie in it!
A termite walks into a bar and asks<>"Is the bar tender here?"
Why did the invisible man turn down the job offer?<> He couldn't see himself doing it.
I used to have a job at a calendar factory <>but I got the sack because I took a couple of days off.
A woman is on trial for beating her husband to death with his guitar collection. Judge says, "First offender?" <>She says, "No, first a Gibson! Then a Fender!”
How do you make holy water?<> You boil the hell out of it.
I had a dream that I was a muffler last night.<> I woke up exhausted!
Did you hear about the circus fire?<> It was in tents!
Don't trust atoms.<> They make up everything!
How many tickles does it take to make an octopus laugh? <>Ten-tickles.
I’m only familiar with 25 letters in the English language.<> I don’t know why.
Why did the cow in the pasture get promoted at work?<> Because he is OUT-STANDING in his field!
What do prisoners use to call each other?<> Cell phones.
Why couldn't the bike standup by itself? <>It was two tired.
Who was the fattest knight at King Arthur’s round table?<> Sir Cumference.
Did you see they made round bails of hay illegal in Wisconsin? <>It’s because the cows weren’t getting a square meal.
You know what the loudest pet you can get is?<> A trumpet.
What do you get when you cross a snowman with a vampire?<> Frostbite.
What do you call a deer with no eyes?<> No idea!
Can February March? <>No, but April May!
What do you call a lonely cheese? <>Provolone.
Why can't you hear a pterodactyl go to the bathroom?<> Because the pee is silent.
What did the buffalo say to his son when he dropped him off at school?<> Bison.
What do you call someone with no body and no nose? <>Nobody knows.
You heard of that new band 1023MB? <>They're good but they haven't got a gig yet.
Why did the crab never share?<> Because he's shellfish.
How do you get a squirrel to like you? <>Act like a nut.
Why don't eggs tell jokes? <>They'd crack each other up.
Why can't a nose be 12 inches long? <>Because then it would be a foot.
Did you hear the rumor about butter? <>Well, I'm not going to spread it!
I made a pencil with two erasers. <>It was pointless.
I used to hate facial hair...<>but then it grew on me.
I decided to sell my vacuum cleaner—<>it was just gathering dust!
I had a neck brace fitted years ago<> and I've never looked back since.
You know, people say they pick their nose,<> but I feel like I was just born with mine.
What do you call an elephant that doesn't matter?<> An irrelephant.
What do you get from a pampered cow? <>Spoiled milk.
It's inappropriate to make a 'dad joke' if you're not a dad.<> It's a faux pa.
How do lawyers say goodbye? <>Sue ya later!
Wanna hear a joke about paper? <>Never mind—it's tearable.
What's the best way to watch a fly fishing tournament? <>Live stream.
I could tell a joke about pizza,<> but it's a little cheesy.
When does a joke become a dad joke?<> When it becomes apparent.
What’s an astronaut’s favorite part of a computer? <>The space bar.
What did the shy pebble wish for?<>That she was a little boulder.
I'm tired of following my dreams. <>I'm just going to ask them where they are going and meet up with them later.
Did you hear about the guy whose whole left side was cut off? <>He's all right now.
Why didn’t the skeleton cross the road? <>Because he had no guts.
What did one nut say as he chased another nut? <> I'm a cashew!
Chances are if you' ve seen one shopping center...<> you've seen a mall.
I knew I shouldn't steal a mixer from work...<>but it was a whisk I was willing to take.
How come the stadium got hot after the game? <>Because all of the fans left.
Why was it called the dark ages? <>Because of all the knights.
Why did the tomato blush? <>Because it saw the salad dressing.
Did you hear the joke about the wandering nun? <>She was a roman catholic.
What creature is smarter than a talking parrot? <>A spelling bee.
I'll tell you what often gets over looked...<> garden fences.
Why did the kid cross the playground? <>To get to the other slide.
Why do birds fly south for the winter?<> Because it's too far to walk.
What is a centipedes's favorite Beatle song? <> I want to hold your hand, hand, hand, hand...
My first time using an elevator was an uplifting experience. <>The second time let me down.
To be Frank...<> I'd have to change my name.
Slept like a log last night … <>woke up in the fireplace.
Why does a Moon-rock taste better than an Earth-rock? <>Because it's a little meteor.
How many South Americans does it take to change a lightbulb?<> A Brazilian
I don't trust stairs.<> They're always up to something.
A police officer caught two kids playing with a firework and a car battery.<> He charged one and let the other one off.
What is the difference between ignorance and apathy?<>I don't know and I don't care.
I went to a Foo Fighters Concert once... <>It was Everlong...
Some people eat light bulbs. <>They say it's a nice light snack.
What do you get hanging from Apple trees? <> Sore arms.
Last night me and my girlfriend watched three DVDs back to back.<> Luckily I was the one facing the TV.
I got a reversible jacket for Christmas,<> I can't wait to see how it turns out.
What did Romans use to cut pizza before the rolling cutter was invented? <>Lil Caesars
My pet mouse 'Elvis' died last night. <>He was caught in a trap..
Never take advice from electrons. <>They are always negative.
Why are oranges the smartest fruit? <>Because they are made to concentrate.
What did the beaver say to the tree? <>It's been nice gnawing you.
How do you fix a damaged jack-o-lantern?<> You use a pumpkin patch.
What did the late tomato say to the early tomato? <>I’ll ketch up
I have kleptomania...<>when it gets bad, I take something for it.
I used to be addicted to soap...<> but I'm clean now.
When is a door not a door?<> When it's ajar.
I made a belt out of watches once...<> It was a waist of time.
This furniture store keeps emailing me,<> all I wanted was one night stand!
How do you find Will Smith in the snow?<> Look for fresh prints.
I just read a book about Stockholm syndrome.<> It was pretty bad at first, but by the end I liked it.
Why do trees seem suspicious on sunny days? <>Dunno, they're just a bit shady.
If at first you don't succeed<> sky diving is not for you!
What kind of music do mummy's like?<>Rap
A book just fell on my head. <>I only have my shelf to blame.
What did the dog say to the two trees? <>Bark bark.
If a child refuses to sleep during nap time...<> are they guilty of resisting a rest?
Have you ever heard of a music group called Cellophane?<> They mostly wrap.
What did the mountain climber name his son?<>Cliff.
Why should you never trust a pig with a secret?<> Because it's bound to squeal.
Why are mummys scared of vacation?<> They're afraid to unwind.
Whiteboards ...<> are remarkable.
What kind of dinosaur loves to sleep?<>A stega-snore-us.
What kind of tree fits in your hand?<> A palm tree!
I used to be addicted to the hokey pokey<> but I turned myself around.
How many tickles does it take to tickle an octopus?<> Ten-tickles!
What musical instrument is found in the bathroom?<> A tuba toothpaste.
My boss told me to attach two pieces of wood together... <>I totally nailed it!
What was the pumpkin’s favorite sport?<>Squash.
What do you call corn that joins the army?<> Kernel.
I've been trying to come up with a dad joke about momentum <>but I just can't seem to get it going.
Why don't sharks eat clowns? <> Because they taste funny.
Just read a few facts about frogs.<> They were ribbiting.
Why didn’t the melons get married?<>Because they cantaloupe.
What’s a computer’s favorite snack?<>Microchips!
Why was the robot so tired after his road trip?<>He had a hard drive.
Why did the computer have no money left?<>Someone cleaned out its cache!
I'm not anti-social. <>I'm just not user friendly.
Why did the computer get cold?<>Because it forgot to close windows.
What is an astronaut's favorite key on a keyboard?<>The space bar!
What's the difference between a computer salesman and a used-car salesman?<>The used-car salesman KNOWS when he's lying.
If at first you don't succeed...<> call it version 1.0
Why did Microsoft PowerPoint cross the road?<>To get to the other slide!
What did the computer do at lunchtime?<>Had a byte!
Why did the computer keep sneezing?<>It had a virus!
What did one toilet say to the other?<>You look a bit flushed.
Why did the picture go to jail?<>Because it was framed.
What did one wall say to the other wall?<>I'll meet you at the corner.
What do you call a boy named Lee that no one talks to?<>Lonely
Why do bicycles fall over?<>Because they are two-tired!
Why was the broom late?<>It over swept!
What part of the car is the laziest?<>The wheels, because they are always tired!
What's the difference between a TV and a newspaper?<>Ever tried swatting a fly with a TV?
What did one elevator say to the other elevator?<>I think I'm coming down with something!
Why was the belt arrested?<>Because it held up some pants!
What makes the calendar seem so popular?<>Because it has a lot of dates!
Why did Mickey Mouse take a trip into space?He wanted to find Pluto!
Why do you go to bed every night?<>Because the bed won't come to you!
What has four wheels and flies?<>A garbage truck!
Why did the robber take a bath before he stole from the bank?<>He wanted to make a clean get away!
Just watched a documentary about beavers.<>It was the best damn program I’ve ever seen.
Slept like a log last night<>woke up in the fireplace.
Why did the scarecrow win an award?<>Because he was outstanding in his field.
Why does a chicken coop only have two doors? <>Because if it had four doors it would be a chicken sedan.
What’s the difference between an African elephant and an Indian elephant? <>About 5000 miles
Why did the coffee file a police report? <>It got mugged.
What did the grape do when he got stepped on? <>He let out a little wine.
How many apples grow on a tree? <>All of them.
What name do you give a person with a rubber toe? <>Roberto
Did you hear about the kidnapping at school? <>It’s fine, he woke up.
Why do scuba divers fall backwards into the water? <>Because if they fell forwards they’d still be in the boat.
How does a penguin build it’s house? <>Igloos it together.
What do you call a man with a rubber toe?<>Roberto
Did you hear about the restaurant on the moon?<>Great food, no atmosphere.
Why was the belt sent to jail?<>For holding up a pair of pants!
Did you hear about the scientist who was lab partners with a pot of boiling water?<>He had a very esteemed colleague.
What happens when a frogs car dies?<>He needs a jump. If that doesn't work he has to get it toad.
What did the flowers do when the bride walked down the aisle?<>They rose.
Why did the man fall down the well?<>Because he couldn’t see that well.
My boss told me to have a good day...<>...so I went home.
How can you tell it’s a dogwood tree?<>By the bark.
Did you hear about the kidnapping at school?<>It’s fine, he woke up.
Why is Peter Pan always flying?<>Because he Neverlands.
Which state has the most streets?<>Rhode Island.
What do you call 26 letters that went for a swim?<>Alphawetical.
Why was the color green notoriously single?<>It was always so jaded.
Why did the coach go to the bank?<>To get his quarterback.
How do celebrities stay cool?<>They have many fans.
What's the most depressing day of the week?<>sadder day.
Dogs can’t operate MRI machines<>But catscan.
I was going to tell a time-traveling joke<>but you guys didn’t like it.
Stop looking for the perfect match<>instead look for a lighter.
I told my doctor I heard buzzing<>but he said it’s just a bug going around.
What kind of car does a sheep like to drive?<>A lamborghini.
What did the accountant say while auditing a document?<>This is taxing.
What did the two pieces of bread say on their wedding day?<>It was loaf at first sight.
Why do melons have weddings?<>Because they cantaloupe.
What did the drummer call his twin daughters?<>Anna One, Anna Two!
What do you call a toothless bear?<> A gummy bear!
Two goldfish are in a tank. <>One says to the other, “Do you know how to drive this thing?”
What’s Forrest Gump’s password?<>1forrest1
What is a child guilty of if they refuse to nap?<> Resisting a rest.
I know a lot of jokes about retired people<>but none of them work.
Why are spiders so smart?<>They can find everything on the web.
What has one head, one foot, and four legs?<> A bed.
What does a house wear?<> Address.
What’s red and smells like blue paint?<>Red paint.
My son asked me to put his shoes on<> but I don’t think they’ll fit me.
I’ve been bored recently, so I decided to take up fencing.<> The neighbors keep demanding that I put it back.
What do you call an unpredictable camera?<>A loose Canon.
Which U.S. state is known for its especially small soft drinks?<>Minnesota.
What do sprinters eat before a race?<> Nothing—they fast.
I’m so good at sleeping...<>I can do it with my eyes closed.
People are usually shocked that I have a Police record.<>But I love their greatest hits!
I told my girlfriend she drew on her eyebrows too high.<> She seemed surprised.
What do you call a fibbing cat?<> A lion.
Why shouldn’t you write with a broken pencil?<> Because it’s pointless.
I like telling Dad jokes…<>sometimes he laughs.
How do you weigh a millennial?<> In Instagrams.
The wedding was so beautiful<>even the cake was in tiers.
What’s the most patriotic sport?<> Flag football.
How do you know when you are going to drown in milk? When its past your eyes!
Milk is also the fastest liquid on earth – its pasteurized before you even see it
A steak pun is a rare medium well done.
Did you hear that the police have a warrant out on a midget psychic ripping people off? It reads "Small medium at large."
A panda walks into a bar and says to the bartender "I'll have a Scotch and . . . . . . . . . . . . . . Coke thank you".
"Sure thing" the bartender replies and asks "but what's with the big pause?"
The panda holds up his hands and says "I was born with them"
A man was caught stealing in a supermarket today while balanced on the shoulders of a couple of vampires. He was charged with shoplifting on two counts.
I heard there was a new store called Moderation. They have everything there
Our wedding was so beautiful, even the cake was in tiers.
Did you hear about the new restaurant on the moon? The food is great, but there's just no atmosphere.
I went to a book store and asked the saleswoman where the Self Help section was, she said if she told me it would defeat the purpose.
What did the mountain climber name his son? Cliff.
"What's ET short for? Because he's only got little legs."
What do you call an Argentinian with a rubber toe? Roberto
What do you call a Mexican man leaving the hospital? Manuel
Today a girl said she recognized me from vegetarian club, but I'm sure I've never met herbivore.
I dreamed about drowning in an ocean made out of orange soda last night. It took me a while to work out it was just a Fanta sea.
I needed a password eight characters long so I picked Snow White and the Seven Dwarfs.
Last night me and my girlfriend watched three DVDs back to back. Luckily I was the one facing the TV.
How do you organize a space party? You planet.
Breaking news! Energizer Bunny arrested – charged with battery.
Conjunctivitis.com – now that's a site for sore eyes.
A Sandwich walks into a bar, the bartender says "Sorry, we don't serve food here"
They laughed when I said I wanted to be a comedian – they're not laughing now.
I'm reading a book on the history of glue – can't put it down.
Where does Napoleon keep his armies? In his sleevies.
I went to the zoo the other day, there was only one dog in it. It was a shitzu.
Why can't you hear a pterodactyl go to the bathroom? The p is silent.
Q: What's 50 Cent's name in Zimbabwe? A: 400 Million Dollars.
"My Dog has no nose." "How does he smell?" "Awful"
What do you call a cow with no legs? Ground beef.
What did the Buffalo say to his little boy when he dropped him off at school? Bison.
So a duck walks into a pharmacy and says "Give me some chap-stick... and put it on my bill"
Why did the scarecrow win an award? Because he was outstanding in his field.
Why did the girl smear peanut butter on the road? To go with the traffic jam.
Why does a chicken coop only have two doors? Because if it had four doors it would be a chicken sedan.
Why don't seagulls fly over the bay? Because then they'd be bay-gulls!
What do you call a fly without wings? A walk.
What do you do when a blonde throws a grenade at you? Pull the pin and throw it back.
What's brown and sounds like a bell? Dung!
How do you make a hankie dance? Put a little boogie in it.
Where does batman go to the bathroom? The batroom.
What's the difference between an African elephant and an Indian elephant? About 5000 miles.
Two muffins were sitting in an oven, and the first looks over to the second, and says, "man, it's really hot in here". The second looks over at the first with a surprised look, and answers, "WHOA, a talking muffin!"
A man walks into a bar and orders helicopter flavor chips. The barman replies "sorry mate we only do plain"
Sgt.: Commissar! Commissar! The troops are revolting! Commissar: Well, you're pretty repulsive yourself.
What do you call a sheep with no legs? A cloud.
I knew i shouldn't have ate that seafood. Because now i'm feeling a little... Eel
What did the late tomato say to the early tomato? I'll ketch up
What did the 0 say to the 8? Nice belt.
Why didn't the skeleton cross the road? Because he had no guts.
Why don't skeletons ever go trick or treating? Because they have nobody to go with.
Why do scuba divers fall backwards into the water? Because if they fell forwards they'd still be in the boat.
Have you ever heard of a music group called Cellophane? They mostly wrap.
What kind of magic do cows believe in? MOODOO.
Wife: Honey I'm pregnant. Me: Well.... what do we do now? Wife: Well, I guess we should go to a baby doctor. Me: Hm.. I think I'd be a lot more comfortable going to an adult doctor.
At what time does the soldier go to the dentist? 1430.
"Hold on, I have something in my shoe" "I'm pretty sure it's a foot"
Why does it take longer to get from 1st to 2nd base, than it does to get from 2nd to 3rd base? Because there's a Shortstop in between!
Dad I'm hungry' ... Hi hungry I'm dad
When phone ringing Dad says 'If it's for me don't answer it.'
Put the cat out ... I didn't realize it was on fire
Where's the bin? Dad: I haven't been anywhere!
Can I watch the TV? Dad: Yes, but don't turn it on.
When Dad drops a pea off of his plate 'oh dear I've pee'd on the table!'
I've been addicted to cold turkey for 2 years. I keep telling people I'm trying to quit cold turkey but nobody is taking me seriously.
Old yachtsmen don't die... They just keel over.
3.14% of sailors are pi-rates.
Bad at golf? Join the club.
I just ate a frozen apple. Hardcore.
Have you met my friend Annette? She's married to a fisherman.
Why is Irish whiskey triple distilled? To be sure, to be sure, to be sure.
I just read a book about Stockholm syndrome. It was pretty bad at first, but by the end I liked it.
RIP boiled water. You will be mist.
Archaeology really is a career in ruins...
I don't trust stairs. They're always up to something.
If you want a job in the moisturiser industry, the best advice I can give is to apply daily.
A big cat escaped it's cage at the zoo yesterday. If I saw that I'd puma pants.
My Czech mate is surprisingly bad at chess.
Why are Lada's so bad? Because the keep Stalin.
What do you get hanging off banana trees? Sore arms.
I made my wife a cocktail with fairy liquid in it.... She was foaming at the mouth when she tasted it.
What do you call a fat psychic? A four-chin teller.
Found out I was colour blind the other day... That one came right out the purple.
I hate perforated lines, they're tearable.
A man tried to sell me a coffin today... I told him that's the last thing I need.
Whenever I want to start eating healthy, a chocolate bar looks at me and Snickers.
Don't kiss your wife with a runny nose. You might think it's funny, but it's snot.
My friend keeps telling me I'm in the closet. I just say it's Narnia business. @WillFerreI
I burnt my Hawaiian pizza last night... I should've put it on aloha setting.
Dad: Where can I get a potato clock? Son: Why a potato clock?!? Dad: I've got a new job and my boss said I need to get-a-potato-clock
My son asked me to stop singing oasis songs in public. I said maybe.
When my wife told me to stop impersonating a flamingo I had to put my foot down.
What's the difference between a hippo and a zippo? One is really heavy, the other is a little lighter.
To the man in the wheelchair that stole my camouflage jacket... You can hide but you can't run.
They don't watch the flintstones in Dubai. But Abu Dhabi do.
Lone Ranger sees Tonto riding with a dustbin. LR: "Where are you going Tonto?" T: "to-the-dump-to-the dump-to-the-dump-dump dump..."
Why can't you hear a pterodactyl using the bathroom? Because the P is silent
Happy Father's Day! Did you hear about the crazy Mexican train thief? He had loco motives
Singing in the shower is all fun and games until you get shampoo in your mouth.... Then it's a soap opera
The rotation of earth really makes my day.
You can't run through a camp site. You can only ran, because it's past tents.
"Does this uniform make me look fat" - insecurity guard
How do you tell the difference between a crocodile and an alligator? You will see one later and one in a while.
I told my wife she drew her eyebrows too high. She seemed surprised.
Why do trees seem suspicious on sunny days? Dunno, they're just a bit shady.
You know what they say about cliffhangers...
Want to hear a joke about construction? Nah, I'm still working on it.
Ever noticed that glass tastes like blood?
A classic from who's line is it anyway.
You heard the rumor going around about butter? Nevermind, I shouldn't spread it.
I have the heart of a lion and a lifetime ban from London zoo. @zsllondonzoo
What did the Buddhist ask the hot dog vendor? "Make me one with everything."
How does the moon cut his hair? Eclipse it!
What do you call an elephant that doesn't matter? An irrelephant
What happened to the cow that jumped over the barbed wire fence? Udder destruction.
I thought about going on an all-almond diet..... But that's just nuts
What's a duck's favourite dip? Quackamole
What do you call a fake noodle? An Impasta
I hate it when people ask me what I will be doing in 5 years time. Come on, I don't have 2020 vision.
Steak puns... They're a rare medium, well done
The shovel was a ground-breaking invention.
Past, present, and future walked into a bar.... It was tense.
Comedians who tell one too many lightbulb jokes soon burn out.
Look! I'm wearing a Thai.
What do you call a Mexican who has lost his car? Carlos.
How does a penguin build it's house? Igloos it together.
Knock knock. Who's there? To. To Who? To whom.
Why do you never see elephants hiding in trees? Because they're so good at it.
I went out with a girl called Simile, I don't know what I metaphor.
I went on a two week holiday to the south of France. It was Toulon.
A pirate walks into a bar with a ship's wheel on his belt buckle. Bartender: What's that on your belt? Pirate: Arrr, It's drivin' me nuts!
Plateaus are the highest form of flattery.
Me: Doctor you've got to help me, I'm addicted to Twitter. Doctor: I don't follow you.
There's no I in denial.
My computer sings, it's a Dell.
It's time to rock around the Christmas tree.
I got a reversible jacket for Christmas, I can't wait to see how it turns out.
I ate a clock yesterday, it was so time consuming.
I'm tired of following my dreams. I'm just going to ask them where they are going and meet up with them later.
What's brown and sticky? A stick.
How do you find Will Smith in the snow? You look for the fresh prints.
Did you hear about the kidnapping at school? Its ok, he woke up.
What's the best thing about elevator jokes? They work on so many levels.
How do you make antifreeze? Steal her blanket.
What's the difference between beer nuts and deer nuts? Beer nuts are about 49cents and deer nuts are just under a buck.
Did you hear about the guy who jumped off a bridge in Paris? He was in Seine.
There are only two types of people in the world, those who can extrapolate from incomplete data...
What did the buffalo say to his son as he left for college? Bison
A truck of Terrapins crashed into a truck of tortoises. It was a turtle disaster.
What does a house wear? A dress.
I asked a Frenchman if he played video games. He said "wii".
Full Meal Jacket
A furniture store keeps calling me. But all I wanted was one night stand.
What did the dog say after a long day at work? "Today was Ruff"
Where are average things built? In the satisfactory.
I've eaten too much Middle Eastern food. Now I falafel.
A pet store had a bird contest. No perches necessary.
What's the worst thing about ancient history class? The teachers tend to Babylon.
Yesterday a clown held a door open for me. I thought it was a nice jester.
How many optometrists does it take to change a light bulb?... 1 or 2? 1... or 2?
My son asked me to take him to the hospital because he had a large red mark on his face. I said "Let's not make any rash decisions."
Just read a few facts about frogs. They were ribbiting.
Sean Connery famously said he would leave The Bahamas and return to Scotland, if it ever gained independence. He must be shitting himself.
I used to work in a shoe recycling shop. It was sole destroying.
The universe implodes. No matter.
I can give you the cause of an anaphylactic shock in a nutshell.
I just swapped our bed for a trampoline. My wife hit the roof.
I heard a rumour that Cadbury is bringing out an oriental chocolate bar. Could be a Chinese Wispa.
My dog Minton ate a shuttlecock... Bad Minton.
Astronomers got tired of watching the moon go round the earth for 24 hours. So the decided to call it a day.
I've got an addiction to water, I think I'm an aquaholic.
What did the hungry clock do? Went back four seconds!
My sea sickness comes in waves.
I play triangle for a reggae band. It's pretty casual. I just stand at the back and ting.
I'm afraid I've caught poetry. Don't worry, I used to suffer from short stories. Really?When? Once upon a time
I asked the checkout girl for a date. She said "They're in the fruit aisle next to the bananas."
What did the chicken say about the scrambled egg? There goes my crazy, mixed up kid.
Why do so many people with laser hair want to get it removed?
What's the difference between a well dressed man on a a bicycle and a poorly dressed man on a tricycle? Attire!
Why does Peter pan always fly?Because he neverlands!
For all American Dads, this is all you need today.
What did the pirate say on his 80th birthday? Aye matey
I jumped into the sea today. My friends pier pressured me into it.
What do you call a sketchy Italian neighbourhood? The Spaghetto.
I have kleptomania, but when it gets bad, I take something for it.
Why can't you have a nose 12 inches long? Because then it would be a foot.
Why do bears have hairy coats? Fur protection.
Someone said my clothes were gay. I said "Yeah, they came out of the closet this morning."
I just misspelt Armageddon, it's not the end of the world.
Volunteering in America is absurd, it just makes no cents.
Jonny Wilkinson is announcing his retirement from rugby. You can't say he didn't try.
Why don't you want to taco bout it? 'Cause i'm nacho friend anymore.
Doorbells, don't knock 'em.
I'm back from holiday in the South Pacific. I wish I had Samoa time off.
"I'm on a whiskey diet, I've lost 4 days already." Tommy Cooper What's your favorite Cooperism?
My wife is on a tropical food diet, the house is full of the stuff. It's enough to make a mango crazy.
Whiteboards are remarkable.
Sweet dreams are made of cheese, who am I to dis a Brie.
Happy Easter! What's your best egg yolk? Mine is: A boiled egg is hard to beat.
What do you call an Alligator wearing a vest? An investigator.
Did you hear about the magic tractor? It turned into a field.
Can February march? No, but April May.
Full credit to the whoever made this for Putin in the effort.
What does a grape say when it is stepped on? Nothing, it just lets out a little wine.
What do you call a dinosaur with an extensive vocabulary? A thesaurus.
I swallowed some Tippex last night. I woke up this morning with a massive correction.
Just got a text from Snoop Dogg. No biggy.
What do you get when you cross a rhetorical question with a joke?
Pink Panthers to do list: To do To do To do, to do, to do To do, to doooo
What did the bra say to the hat? You go on ahead, I'll give these two a lift.
What did one eye say to the other? Something smells between us.
Two elephants fall off a cliff... Boom boom!
How many tickles does it take to make an octopus laugh? Tentacles.
I don't like atoms, they're liars. They make up everything.
If you want to set up a company and run it, then that's your own business.
My friend is going on holiday to the Middle East. Oman, that sounds fun...
Whoever invented the door knocker deserves a no-bell prize!
Why did the elf push his bed into the fireplace? He wanted to sleep like a log.
I can't stand Russian dolls.... They're so full of themselves.
I remember the first time I saw a universal remote controller. I thought to myself "well, this changes everything..."
I got this extra electron I didn't want. My friend said "don't be so negative."
A boat builder is showing his son one of his forests. He turns to him and says, "Son, one day this will all be oars"
Molestation is a touchy subject.
I’ve decided to put up a marquee in my garden with some funky music and flashing lights. Now is the winter of my disco tent.
I was thinking about moving to Moscow but there is no point Russian into things.
I met a Dutch girl with inflatable shoes last week, I phoned her up for a date but she'd popped her clogs.
My New Years resolution is to stop leaving things so late.
Did you hear about the man who gave up making haggis? He didn't have the guts for it anymore.
Retrospective baddadjoke: Why are there no pain killers in the jungle? Because parrots-eat-em-all
Sometimes I squat on the floor, put my arms around my legs and lean forward. That's how I roll.
Got lost in a corn field today, it was a-maize-ing.
I needed a password eight characters long so I picked Snow White and the Seven Dwarves.
Just out buying some new chairs for the house, sofa so good.
My wife told me I was average, I think she's mean.
I was going to tell a dairy joke, but it was too cheesy.
Just had my first round of golf. I'm not very good, in fact I've got a fairway to go.
My daughter just lost her mood ring, really don't know how she feels about it.
I told a friend I was off to California this summer. He told me to be more pacific... so I went to Hawaii instead...
I gave all my dead batteries away today... Free of charge.
Why is there a long line at the cemetery? Because people are dying to get in.
Why did the can crusher quit his job? Because it was soda pressing.
I'm starting a band called 1023mb We'll never get a gig.
What's Forest Gump's Facebook password? 1forest1
A photon checks into a hotel. Receptionist: "May I take your bags sir?" Photon: "I don't have any bags, I'm travelling light."
Melon 1: "Let's run away and get married." Melon 2: "Sorry but I Cantaloupe."
Did you hear about the Italian chef who died? He pasta way.
I lost my job last week. Unemployment is not working for me.
A termite walks into a bar and asks "Is the bar tender here?"
Hitler was surprised by the Invasion of Normandy. He did nazi that coming.
A Freudian slip is when you say one thing but mean your mother.
So, I asked my North Korean mate how his life was going? He said "can't complain"
Just quit my job at Starbucks because day after day it was the same old grind.
I went to the zoo the other day, there was only one dog in it, it was a shitzu.
Why is Saudi Arabia free of mental illness? Because No-mad people live there.
Without geometry life is pointless.
I broke my guitar string last night. Don't fret, I had another.
Had a new beaver curry last night. It's like a normal curry, just a bit 'otter.
Went to the corner shop today... Bought four corners.
Have you heard the conspiracy about Russian allotments. It's all just a communist plot.
My uncle works with Digital radios. You could say he’s a DAB hand.
I dreamt about drowning in an ocean made out of orange soda last night. It took me a while to work out it was just a Fanta sea.
My cat was just sick on the carpet, I don't think it's feline well.
Why do the French only put one egg in an omelette? Because one egg is un oeuf.
The other day someone left plasticine in my house. I didn't know what to make of it.
What happens when you tell an egg a joke? It cracks up.
How do you make holy water? Boil the hell out of it.
Sorry I've been away for a while, I was at the fabric shop looking for new material.
I've just been to a very emotional wedding. Even the cake was in tiers.
When you have a bladder infection, urine trouble.
I stayed up all night to find out where the sun went, then it dawned on me...
I went to the doctor today and he told me I had type A blood but it was a type O.
Today a girl said she recognised me from vegetarian club, but I'm sure I've never met herbivore.
Jokes about German sausages are the wurst.
I tried to throw a ball at a cloud. I mist.
I woke up with a face full of rice. I must've fallen asleep as soon as my head hit the pilau.
I couldn't pay for my coffee because my wallet was in my other pair of moccachinos. I got it for free. Thanks a latté @lashingsbristol!
I cut my finger chopping cheese, but I think that I may have grater problems.
First rule of Thesaurus Club. You don't talk, converse, discuss, speak, chat, deliberate, confer, gab, gossip or natter about Thesaurus Club
Don't have a Findus lasagne before bed. You'll have a nightMARE.
How does a muppet die? Apparently, it kermits suicide.
What did the Mexican say to his chicken? Oh-lay!
A pet shop was ransacked last week... ...there are currently no leads.
How do you drown a hipster? In the mainstream.
Sleeping comes naturally to me. I can do it with my eyes closed.
I ate some rotten chicken last night. Now I feel fowl.
There is a new disease found in margarine... Apparently it spreading very easily.
What do you call an Italian with a rubber toe? Roberto
Why do crabs never give to charity? Because they're shellfish.
What is Santa's favourite pizza? One that's deep pan, crisp and even.
People are making apocalypse jokes like there's no tomorrow.
Someone called me pretentious the other day... I almost choked on my latte.
My mate dug a hole in the garden and filled it with water....I think he meant well.
What's your favourite Christmas Cracker Joke? Here's one of mine: "What's ET short for? Because he's only got little legs."
If you're struggling to think of what to get someone for Christmas. Get them a fridge and watch their face light up when they open it.
A mate of mine has admitted to being addicted to break fluid. I'm worried but he says he can stop whenever he wants.
Start a new job in Seoul next week. I thought it was a good Korea move.
Soya Milk. Looked in your fridge.
A book just fell on my head. I've only got myshelf to blame.
Bloody thespians, always making a scene.
My dad fought in the war and survived mustard gas and pepper spray. He is now classed as a seasoned veteran.
Tea is for mugs.
This thesaurus isn't just terrible, it is also terrible.
I am terrified of elevators. I'm going to start taking steps to avoid them.
Need an ark to save two of every animal? I Noah guy.
What did the father say to the son who was going fishing? Let minnow when you get there.
I am delighted with the corn crop this year. It's A-maize-ing.
How does Moses make his tea? Hebrews it.
I think rowing is oarsome.
What's the advantage of living in Switzerland? Well, the flag is a big plus.
Nostalgia isn't what it used to be.
Why do accountants look so good in heels? Because they never lose their balance.
I'll stop at nothing to avoid using negative numbers.
Wind turbines. I'm a big fan!
What's the definition o a good farmer? A man outstanding in his field.
Why did the octopus beat the shark in a fight? Because it was well armed.
Why does a Moon-rock taste better than an Earth-rock? Because it's a little meteor.
I fired my masseuse today. She rubbed me up the wrong way.
A red and a blue ship have just collided in the Caribbean. Apparently the survivors are marooned.
Breaking news! A hurricane has just hit the the main cheese factory in France. All that's left is de-Brie.
I'm glad I know sign language, it's pretty handy.
I like sea food. I often just have it for the halibut.
A girl walks into a bar and asked for a double entendre. So the barman gave her one.
I took the shell off of my racing snail to see if it went any faster. If anything though, it just made it more sluggish.
I've deleted the phone numbers of all the Germans I know from my mobile phone. Now it's Hans free.
Was kept awake last night by someone flashing a light in my face. It was torch-ure.
My wife said to me "Your lack of originality is pathetic."I said "Yeah, well your lack of originality is pathetic."
It was really hard overcoming my addiction to the hokey cokey. But I turned myself around and that’s what it’s all about.
"I saw a documentary on how ships are kept together. Riveting!" Stewart Francis
Last night me and my girlfriend watched three DVDs back to back. Luckily I was the one facing the telly.
My wife just split up with me because I've got a pasta fetish. I'm feeling cannelloni right now.
I'm thinking about getting a new haircut... I'm going to mullet over.
Had a bowl of scotch broth for lunch today... It was souper hot.
I got really quick service at the fish and chip shop. It was very e-fish-ent
How do you organise a space party? You planet.
What did one bird say to the other cheating parrot? Toucan play at that game.
What's wrong with the Southern French's trousers? They're Toulouse.
How much does a hipster weigh? An instagram.
A photon enters a hotel. Porter: 'Need any help with your luggage?' Photon: 'No thanks, I'm travelling light'
Give me ambiguity or give me something else.
A banker came home from work today worried about his job. He said its in the balance.
sorry "a *pod* of killer whales"
What do you call a group of killer whales playing instruments? An Orca-stra.
The only thing that can survive a
A man has taken @British_Airways to court after they misplaced his luggage. He lost his case.
Did you hear about the guy whose whole left side was cut off? He's all right now.
Why was the big cat disqualified from the race? Because it was a cheetah.
What do you call a man with rabbits living in his bum? Warren
Just been fishing... It was reely good.
A man walked in to a bar with some asphalt on his arm. He said "Two beers please, one for me and one for the road."
It's so hard to think of another chemistry joke... All the good ones Argon.
Why do people dislike mushrooms? Because they're made from Toads Stools...
There was so much fighting on our Easter camping trip... it was in-tents.
It's easter already?!
I'm off to Nairobi in the Summer. Kenya believe it?
My first girlfriend's name was Ivy... she was all over me.
I've just voted for Charlie's odyssey by Charlie Denholm as the funniest film
Helvetica walks into a bar. The barman says "We don't serve your type around here."
Argon walks into a bar. The barman says "Get the hell out!" Argon doesn't react.
Just watched a documentary about beavers... It was the best damn program I've ever seen.
Last night it was raining cats and dogs... I stepped in a poodle.
I thought about being a juggler, but I didn't have the balls.
My mate got a job as a lion's hairdresser at the zoo today. He is literally the mane man.
I'm not as think as you drunk I am.
I'm thinking about moving to France... I've got nothing Toulouse.
Went surfing the other day, it was swell.
Watershed joke: A baker was caught bonking his bread loaves. They say he was inbread.
The only thing that can survive a double dip is a hobnob. Osborne, call McVities.
I enjoy using the comedy technique of self-deprecation – but I’m not very good at it.
My wife... its difficult to say what she does... She sells seashells on the seashore.
A poker player loses his arm in a nasty accident. He's now got a prosthetic replacement. He just can't deal with it.
A girl invited me back to her place last night for champagne... It turned out it was real pain.
Theres a new type of pillow made from corduroy... Its making headlines.
What did the father say to his crying son at his Indian themed birthday party? It's chapatti and you can cry if you want to.
Breaking news! Energizer Bunny arrested - charged with battery.
Wow who saw that coming? Harry Potter and News of the World two of the Biggest selling modern fiction publications ending in the same week.
I went in to a pet shop. I said 'Can I buy a goldfish?' The guy said, 'Do you want an aquarium?' I said 'I don't care what star sign it is.'
A man was found today vacuum cleaning the top of nelsons column without any safety equipment. Police say he was Dyson with death.
A man went to A&E at the weekend who swallowed 12 plastic horses. Don't worry the doctors describe his condition as stable.
Conjunctivitis.com - now that's a site for sore eyes.
A guy walks into the psychiatrist wearing only clingfilm for shorts. The shrink says, "Well, I can clearly see you're nuts."
WIMBLEDON SPECIAL Why should you never fall in love with a tennis player? To them, "Love" means nothing.
I went to the doctor the other day I said 'have you got anything for wind' so he gave me a kite.
A sandwich walks into a bar. The barman says "we don't serve food here."
The recruitment consultant asked me "What do you think of voluntary work?" I said "I wouldn't do it if you paid me."
An ice cream man was found lying on the floor of his van covered with hundreds and thousands. Police say that he topped himself.
"Doctor, I've broken my arm in several places" Doctor "Well don't go to those places."
A boiled egg in the morning is hard to beat.
I'm on a whiskey diet. I've lost three days already.
What do you do with chemists when they die? We barium.
Pretty appropriate. Seven days without a pun makes one weak.
Hand me my Mondeo, my semidetached house, my unloved wife, my unfulfilling job, my xbox kids. Twitter, I am your dad and I tell bad jokes.
What type of onion is the best painkiller? A-sprin' onion...
Just passed a manicurist and a dentist quarreling in the street- they were fighting tooth and nail.
I fear for the calendar, it's days are numbered.
What's the definition of 'A Will'? (I'll give you a clue, it's a dead giveaway.)
I buy a different brand of cling flim every time I go to the shops. Just to keep things fresh.
The advantages of origami are twofold.
There's a new type of broom out, it's sweeping the nation.
Atheism is a non-prophet organisation.
I went to a seafood disco last night and pulled a muscle.
My friend drowned in a bowl of muesli. A strong currant pulled him in.
Sometimes I drink my whiskey neat. Other times I take off my tie and untuck my shirt.
I don't want to sound big headed but I wear extra large hats.
My friend said "You remind me of a ketchup bottle", I said "I'll take that as a condiment".
Slept like a log last night ... woke up in the fireplace.
Exit signs - they're on the way out aren't they.
What did the fish say when it swam into a wall? Damn!
A cat hijacked a plane, stuck a pistol to the pilots ribs and said "TAKE ME TO THE CANARIES!"
They laughed when I said I wanted to be a comedian - they're not laughing now.
One arm butlers - they can take it, but they can't dish it out.
A shark will only attack you if you're wet
Beware of alphabet grenades, they might spell disaster.
What cheese can never be yours? Nacho cheese.
Last night I saw this guy chatting up a cheetah at the bar. I thought 'he's trying to pull a fast one.'
My housemate opened the fridge last night and threw a block of cheese at me. I said "That's mature."
A police officer caught two kids playing with a firework and a car battery. He charged one and let the other one off.
I used to be indecisive, but now I'm not quite sure.
Why are there no pain killers in the jungle? Because parrots-eat-em-all
I'm reading a book on the history of glue - can't put it down.
Albinos - can't say fairer than that.
Velcro... What a rip-off.
A man walks into a butcher. The butcher bets him £5 he can't guess a shelf of meat's weight. Man replies "I cant, the steaks are too high."
Two aerials meet on a roof, fall in love and get married... The ceremony was rubbish but the reception was excellent.
Black beauty... He's a dark horse.
================================================
FILE: main.py
================================================
import argparse
import importlib
import json
import multiprocessing
import os
import sys
import time
from prettytable import PrettyTable
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from util.config import Config, manage_config
from util.logger import Logger
from util.notification import ErrorNotifyHandler
from util.scheduler import check_schedule
from util.utility import create_bar
from util.version import get_version, start_version_check
list_of_python_modules = [
"border_replacerr",
"health_checkarr",
"labelarr",
"nohl",
"poster_cleanarr",
"poster_renamerr",
"renameinatorr",
"sync_gdrive",
"upgradinatorr",
"unmatched_assets",
"jduparr",
]
class ScheduleFileHandler(FileSystemEventHandler):
def __init__(self, callback, debounce_interval=1):
super().__init__()
self.callback = callback
self.last_modified = 0
self.debounce_interval = debounce_interval
def on_modified(self, event):
sys.stderr.write(f"[WATCHDOG] Detected change in: {event.src_path}\n")
if event.src_path.endswith("config.yml"):
now = time.time()
if now - self.last_modified > self.debounce_interval:
self.last_modified = now
self.callback()
def start_schedule_watcher(callback):
observer = Observer()
observer.daemon = True
handler = ScheduleFileHandler(callback)
# Determine config directory and ensure it exists
config_path = os.environ.get("CONFIG_DIR", "./config")
if not os.path.isdir(config_path):
os.makedirs(config_path, exist_ok=True)
observer.schedule(handler, path=config_path, recursive=False)
observer.start()
return observer
class ModuleManager:
def __init__(self, logger):
self.running_modules = {}
self.module_start_times = {}
self.logger = logger
self.last_run_times = {}
def run(self, module_name, run_module):
process = run_module(module_name, self.logger)
if process:
self.running_modules[module_name] = process
self.module_start_times[module_name] = time.time()
def run_if_due(self, module_name, schedule_time, check_schedule_func, run_module):
if check_schedule_func(module_name, schedule_time, self.logger):
import time
now = time.time()
last_run = self.last_run_times.get(module_name, 0)
# Prevent multiple runs within the same schedule window (60 seconds)
if now - last_run >= 60:
self.logger.info(
f"[SCHEDULE] Running module: {module_name} at {schedule_time}"
)
self.last_run_times[module_name] = now
self.run(module_name, run_module)
def is_already_running(self, module_name):
return module_name in self.running_modules
def cleanup(self):
processes_to_remove = []
for module_name, process in self.running_modules.items():
if process and not process.is_alive():
duration = time.time() - self.module_start_times.pop(module_name)
self.logger.info(
f"[SCHEDULE] module: {module_name.upper()} has finished in {duration:.2f} seconds"
)
processes_to_remove.append(module_name)
for module_name in processes_to_remove:
del self.running_modules[module_name]
def has_running_modules(self):
return bool(self.running_modules)
def load_schedule():
# Do not merge defaults here; only read existing config
config = Config("main")
schedule = config.scheduler
return schedule
def run_module(module_to_run, output=False, logger=None):
config = Config(module_to_run).module_config
def run_python_module(module_to_run):
config.instances_config = Config(module_to_run).instances_config
module = importlib.import_module(f"modules.{module_to_run}")
process = multiprocessing.Process(target=module.main, args=(config,))
process.start()
return process
if module_to_run in list_of_python_modules:
return run_python_module(module_to_run)
def print_schedule(logger, modules_schedules):
logger.info(create_bar("SCHEDULE"))
table = PrettyTable(["module", "Schedule"])
table.align = "l"
table.padding_width = 1
for module_name, schedule_time in modules_schedules.items():
table.add_row([module_name, schedule_time])
logger.info(f"{table}")
logger.info(create_bar("SCHEDULE"))
def main():
# CLI argument parsing
parser = argparse.ArgumentParser(description="Run DAPS modules or start web UI.")
parser.add_argument(
"modules", nargs="*", help="Module names to run once (cli mode)."
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s {get_version()}",
help="Show the DAPS version and exit.",
)
args = parser.parse_args()
# Set console logging: modules only when explicitly requested via CLI,
if args.modules:
os.environ["LOG_TO_CONSOLE"] = "true"
else:
os.environ["LOG_TO_CONSOLE"] = "false"
if args.modules:
# CLI mode: run specified modules and exit
for name in args.modules:
if name in list_of_python_modules:
run_module(name, output=True)
else:
print(f"Error: module '{name}' not found.")
sys.exit(1)
return
try:
main_config = Config("main").module_config
except Exception as e:
print(f"Error loading main config for logger: {e}")
sys.exit(1)
logger = Logger(main_config.log_level, "main")
main_logger = logger._logger if hasattr(logger, "_logger") else logger
error_notify_handler = ErrorNotifyHandler(
main_config, module_name="main", logger=main_logger
)
main_logger.addHandler(error_notify_handler)
manage_config(logger)
# Web mode: no modules passed
initial_run = True
waiting_message_shown = False
# Load main config once and reuse for scheduling reloads
main_cfg = Config("main")
def on_schedule_change():
try:
main_cfg.load_config()
new_schedule = main_cfg.scheduler
except Exception as e:
logger.error(f"[MAIN] Error reloading config: {e}", exc_info=True)
return
nonlocal current_schedule
if new_schedule != current_schedule:
current_schedule = new_schedule
schedule_changed.set()
try:
current_schedule = main_cfg.scheduler
except Exception as e:
logger.error(f"Error loading schedule: {e}", exc_info=True)
sys.exit(1)
if not isinstance(current_schedule, dict):
print(f"❌ Schedule is not a dictionary: {current_schedule}")
sys.exit(1)
import atexit
import threading
schedule_changed = threading.Event()
observer = start_schedule_watcher(on_schedule_change)
atexit.register(observer.stop)
# Give the observer up to 2 seconds to finish
atexit.register(lambda: observer.join(timeout=2))
try:
from web.server import start_web_server
if main_config.update_notifications:
start_version_check(main_config, logger, interval=3600)
start_web_server(logger)
manager = ModuleManager(logger)
# Expose the ModuleManager to the web server for status/cancel of scheduled tasks
import web.server
web.server.app.state.manager = manager
while True:
if initial_run or schedule_changed.is_set():
print_schedule(logger, current_schedule)
logger.debug(
f"📋 Current schedule contents:\n{json.dumps(current_schedule, indent=4)}"
)
schedule_changed.clear()
initial_run = False
waiting_message_shown = False
if not waiting_message_shown:
logger.info("[SCHEDULE] Waiting for scheduled modules...")
waiting_message_shown = True
for module_name, schedule_time in current_schedule.items():
if manager.is_already_running(module_name) or not schedule_time:
continue
if module_name in list_of_python_modules:
manager.run_if_due(
module_name, schedule_time, check_schedule, run_module
)
manager.cleanup()
time.sleep(5)
except KeyboardInterrupt:
logger.info("Keyboard Interrupt detected. Shutting DAPS down...")
sys.exit()
except Exception:
logger.error("\n\nAn error occurred:\n", exc_info=True)
if __name__ == "__main__":
main()
================================================
FILE: modules/__init__.py
================================================
================================================
FILE: modules/border_replacerr.py
================================================
import filecmp
import logging
import os
import shutil
import sys
from datetime import datetime
from types import SimpleNamespace
from typing import Any, Dict, List, Optional, Tuple, Union
from util.assets import get_assets_files
from util.logger import Logger
from util.utility import create_table, get_log_dir, print_json, print_settings, progress
try:
from PIL import Image, UnidentifiedImageError
except ImportError as e:
print(f"ImportError: {e}")
print("Please install the required modules with 'pip install -r requirements.txt'")
exit(1)
logging.getLogger("PIL").setLevel(logging.WARNING)
def load_last_run(log_dir: str, logger: Logger = None) -> Optional[datetime]:
"""
Load the last run timestamp from the .last_run file in the log directory.
"""
try:
last_run_file = os.path.join(log_dir, ".last_run")
if os.path.exists(last_run_file):
with open(last_run_file, "r") as f:
ts = f.read().strip()
try:
return datetime.fromisoformat(ts)
except Exception:
pass
except Exception as e:
logger.error(f"Failed to read file: {e}")
return None
def save_last_run(log_dir: str, dt: Optional[datetime] = None, logger: Logger = None) -> None:
"""
Save the current timestamp (or provided datetime) to the .last_run file in the log directory.
"""
try:
last_run_file = os.path.join(log_dir, ".last_run")
now = dt or datetime.now()
with open(last_run_file, "w") as f:
f.write(now.isoformat())
except Exception as e:
logger.error(f"Failed to write file: {e}")
def check_holiday(
config: SimpleNamespace, logger: Logger, last_run: Optional[datetime] = None
) -> Tuple[bool, Optional[List[str]], Dict[str, bool]]:
"""
Determines if today falls within a holiday schedule and returns applicable border colors and switch flags.
Now supports modules run less than daily by checking if a holiday started or ended since the last run.
Args:
config (SimpleNamespace): Configuration object containing holidays.
logger (Logger): Logger instance for logging messages.
last_run (Optional[datetime]): Timestamp of last module run.
Returns:
Tuple[bool, Optional[List[str]], Dict[str, bool]]:
- True if today is a holiday, else False.
- List of border colors if a holiday is active, else None.
- Dictionary indicating if a holiday started or ended since last run.
"""
holiday_switch: Dict[str, bool] = {
"start_since_last_run": False,
"end_since_last_run": False,
}
now = datetime.now()
for holiday, schedule_color in config.holidays.items():
schedule = schedule_color.get("schedule")
if not schedule or not schedule.startswith("range("):
continue
inside = schedule[len("range(") : -1]
start_str, end_str = inside.split("-", 1)
sm, sd = map(int, start_str.split("/"))
em, ed = map(int, end_str.split("/"))
year = now.year
# Handle ranges that cross the year boundary
start_date = datetime(year, sm, sd)
end_date = datetime(year, em, ed)
if end_date < start_date:
# Range crosses new year
if now.month < sm:
start_date = start_date.replace(year=year - 1)
else:
end_date = end_date.replace(year=year + 1)
# Check if holiday started or ended since last run
if last_run:
if start_date > last_run and start_date <= now:
holiday_switch["start_since_last_run"] = True
if end_date > last_run and end_date <= now:
holiday_switch["end_since_last_run"] = True
else:
# On first run, treat as start if within holiday
if start_date <= now <= end_date:
holiday_switch["start_since_last_run"] = True
# Is today inside the holiday range?
if start_date <= now <= end_date:
holiday_colors = schedule_color.get("color", config.border_colors)
if isinstance(holiday_colors, str):
holiday_colors = [holiday_colors]
logger.info(create_table([[f"Running {holiday.capitalize()} Schedule"]]))
logger.info(
f"Schedule: {holiday.capitalize()} | Using {', '.join(holiday_colors)} border colors."
)
return True, holiday_colors, holiday_switch
return False, None, holiday_switch
def convert_to_rgb(hex_color: str, logger: Logger) -> Tuple[int, int, int]:
"""
Converts a hexadecimal color string to an RGB tuple.
Args:
hex_color (str): Hexadecimal color string.
logger (Logger): Logger instance for logging errors.
Returns:
Tuple[int, int, int]: RGB color tuple.
"""
hex_color = hex_color.strip("#")
if len(hex_color) == 3:
hex_color = hex_color * 2
try:
color_code = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
except ValueError:
logger.error(
f"Error: {hex_color} is not a valid hexadecimal color code.\nDefaulting to white."
)
return (255, 255, 255)
return color_code
def fix_borders(
assets_dict: Union[List[Dict[str, Any]], Dict[str, List[Dict[str, Any]]]],
config: SimpleNamespace,
border_colors: Optional[List[str]],
destination_dir: str,
dry_run: bool,
logger: Logger,
exclusion_list: Optional[List[str]],
) -> List[str]:
"""
Processes image assets and applies or removes borders based on configuration.
Args:
assets_dict (Dict[str, List[Dict[str, Any]]]): Dictionary of assets categorized by type.
config (SimpleNamespace): Module configuration.
border_colors (Optional[List[str]]): List of border colors to use.
destination_dir (str): Target output directory.
dry_run (bool): If True, simulate changes without saving.
logger (Logger): Logger instance for logging messages.
exclusion_list (Optional[List[str]]): List of items to exclude from processing.
Returns:
List[str]: Status messages for each processed asset.
"""
# Support flat list or grouped dict by type
if isinstance(assets_dict, list):
grouped: Dict[str, List[Dict[str, Any]]] = {}
for asset in assets_dict:
asset_type = asset.get("type")
grouped.setdefault(asset_type, []).append(asset)
assets_dict = grouped
rgb_border_colors: List[Tuple[int, int, int]] = []
if border_colors:
for color in border_colors:
rgb_color = convert_to_rgb(color, logger)
rgb_border_colors.append(rgb_color)
if not border_colors:
action = "Removed border"
banner = "Removing Borders"
else:
action = "Replaced border"
banner = "Replacing Borders"
if action:
table = [
[f"{banner}"],
]
logger.info(create_table(table))
messages: List[str] = []
for key, items in assets_dict.items():
current_index = 0
if not items:
logger.info(f"No {key} found in the input directory")
continue
with progress(
items,
desc=f"Processing {key.capitalize()}",
total=len(items),
unit=" items",
logger=logger,
leave=True,
) as pbar:
for data in pbar:
files = data.get("files", None)
year = data.get("year", None)
folder = data.get("folder", None)
if year:
year_str = f"({year})"
else:
year_str = ""
excluded = False
if exclusion_list and f"{data['title']} {year_str}" in exclusion_list:
excluded = True
logger.debug(f"Excluding {data['title']} {year_str}")
# Prepare output directory for saving processed files
output_path = destination_dir
for input_file in files:
file_name, extension = os.path.splitext(input_file)
if extension not in [
".jpg",
".png",
".jpeg",
".JPG",
".PNG",
".JPEG",
]:
logger.warning(
f"Skipping {input_file} as it is not a jpg or png file."
)
continue
file_name = os.path.basename(input_file)
if rgb_border_colors:
rgb_border_color = rgb_border_colors[current_index]
else:
rgb_border_color = None
if not dry_run:
if rgb_border_color:
results = replace_borders(
input_file,
output_path,
rgb_border_color,
config.border_width,
folder,
logger,
)
else:
results = remove_borders(
input_file,
output_path,
config.border_width,
logger,
excluded,
folder,
)
if results:
messages.append(f"{action} on {file_name}")
else:
messages.append(f"Would have {action} on {file_name}")
if rgb_border_colors:
current_index = (current_index + 1) % len(rgb_border_colors)
pbar.update(1)
return messages
def replace_borders(
input_file: str,
output_path: str,
border_colors: Tuple[int, int, int],
border_width: int,
folder: Optional[str],
logger: Logger,
) -> bool:
"""
Removes the existing border and applies a new one with the specified color.
Args:
input_file (str): Path to the input image file.
output_path (str): Path to save the processed image.
border_colors (Tuple[int, int, int]): RGB color for the new border.
border_width (int): Width of the border to apply.
folder (Optional[str]): Subfolder to organize output files.
logger (Logger): Logger instance for logging messages.
Returns:
bool: True if the file was saved or updated; False otherwise.
"""
try:
with Image.open(input_file) as image:
width, height = image.size
# Remove border by cropping
cropped_image = image.crop(
(
border_width,
border_width,
width - border_width,
height - border_width,
)
)
# Add border by expanding the canvas
new_width = cropped_image.width + 2 * border_width
new_height = cropped_image.height + 2 * border_width
final_image = Image.new("RGB", (new_width, new_height), border_colors)
final_image.paste(cropped_image, (border_width, border_width))
file_name = os.path.basename(input_file)
if folder:
final_path = f"{output_path}/{folder}/{file_name}"
else:
final_path = f"{output_path}/{file_name}"
final_image = final_image.resize((1000, 1500)).convert("RGB")
if os.path.isfile(final_path):
# Only save if the file is different to avoid unnecessary overwrites
tmp_path = f"/tmp/{file_name}"
final_image.save(tmp_path)
if not filecmp.cmp(final_path, tmp_path):
final_image.save(final_path)
os.remove(tmp_path)
return True
else:
os.remove(tmp_path)
return False
else:
if not os.path.exists(os.path.dirname(final_path)):
os.makedirs(os.path.dirname(final_path), exist_ok=True)
final_image.save(final_path)
return True
except UnidentifiedImageError as e:
logger.error(f"Error: {e}")
logger.error(f"Error processing {input_file}")
return False
except Exception as e:
logger.error(f"Error: {e}")
logger.error(f"Error processing {input_file}")
return False
def remove_borders(
input_file: str,
output_path: str,
border_width: int,
logger: Logger,
exclude: bool,
folder: Optional[str],
) -> bool:
"""
Crops an image to remove its borders and optionally adds a black bottom border.
Args:
input_file (str): Path to the input image file.
output_path (str): Path to save the processed image.
border_width (int): Width of the border to remove.
logger (Logger): Logger instance for logging messages.
exclude (bool): If True, remove all borders; if False, add black bottom border.
folder (Optional[str]): Subfolder to organize output files.
Returns:
bool: True if the file was saved or updated; False otherwise.
"""
try:
with Image.open(input_file) as image:
width, height = image.size
if not exclude:
# Remove top, left, right borders, add black bottom border
final_image = image.crop(
(border_width, border_width, width - border_width, height)
)
bottom_border = Image.new(
"RGB", (width - 2 * border_width, border_width), color="black"
)
bottom_border_position = (0, height - border_width - border_width)
final_image.paste(bottom_border, bottom_border_position)
else:
# Remove all borders
final_image = image.crop(
(
border_width,
border_width,
width - border_width,
height - border_width,
)
)
final_image = final_image.resize((1000, 1500)).convert("RGB")
file_name = os.path.basename(input_file)
if folder:
final_path = f"{output_path}/{folder}/{file_name}"
else:
final_path = f"{output_path}/{file_name}"
if os.path.isfile(final_path):
tmp_path = f"/tmp/{file_name}"
final_image.save(tmp_path)
if not filecmp.cmp(final_path, tmp_path):
final_image.save(final_path)
os.remove(tmp_path)
return True
else:
os.remove(tmp_path)
return False
else:
if not os.path.exists(os.path.dirname(final_path)):
os.makedirs(os.path.dirname(final_path), exist_ok=True)
final_image.save(final_path)
return True
except UnidentifiedImageError as e:
logger.error(f"Error: {e}")
logger.error(f"Error processing {input_file}")
return False
except Exception as e:
logger.error(f"Error: {e}")
logger.error(f"Error processing {input_file}")
return False
def copy_files(
assets_dict: Dict[str, List[Dict[str, Any]]],
destination_dir: str,
dry_run: bool,
logger: Logger,
) -> List[str]:
"""
Copies asset files from the input to the output directory with change detection.
Args:
assets_dict (Dict[str, List[Dict[str, Any]]]): Dictionary of asset data.
destination_dir (str): Path to the output directory.
dry_run (bool): Whether to simulate copying without actual file write.
logger (Logger): Logger instance for logging.
Returns:
List[str]: A list of copy operations performed or simulated.
"""
messages: List[str] = []
if destination_dir.endswith("/"):
destination_dir = destination_dir.rstrip("/")
asset_types = ["movies", "series", "collections"]
for asset_type in asset_types:
if asset_type in assets_dict:
items = assets_dict[asset_type]
with progress(
items,
desc=f"Processing {asset_type.capitalize()}",
total=len(items),
unit=" items",
logger=logger,
leave=True,
) as pbar:
for data in pbar:
files = data.get("files", None)
year = data.get("year", None)
if year:
year_str = f"({year})"
else:
year_str = ""
output_path = destination_dir
for input_file in files:
file_name, extension = os.path.splitext(input_file)
if extension not in [
".jpg",
".png",
".jpeg",
".JPG",
".PNG",
".JPEG",
]:
logger.warning(
f"Skipping {input_file} as it is not a jpg or png file."
)
continue
file_name = os.path.basename(input_file)
final_path = f"{output_path}/{file_name}"
output_basename = os.path.basename(output_path)
if not dry_run:
if os.path.isfile(final_path):
if not filecmp.cmp(final_path, input_file):
try:
shutil.copy(input_file, final_path)
except shutil.SameFileError:
logger.debug(
f"Input file {input_file} is the same as {final_path}, skipping"
)
logger.debug(
f"Input file {input_file} is different from {final_path}, copying to {output_basename}"
)
messages.append(
f"Copied {data['title']}{year_str} - {file_name} to {output_basename}"
)
else:
try:
shutil.copy(input_file, final_path)
except shutil.SameFileError:
logger.debug(
f"Input file {input_file} is the same as {final_path}, skipping"
)
logger.debug(
f"Input file {input_file} does not exist in {output_path}, copying to {output_basename}"
)
messages.append(
f"Copied {data['title']}{year_str} - {file_name} to {output_basename}"
)
else:
messages.append(
f"Would have copied {data['title']}{year_str} - {file_name} to {output_basename}"
)
pbar.update(1)
return messages
def process_files(
source_dirs: str,
config: SimpleNamespace,
logger: Optional[Logger] = None,
renamerr_config: Optional[SimpleNamespace] = None,
renamed_assets: Optional[Dict[str, Any]] = None,
incremental_run: Optional[bool] = False,
) -> None:
"""Main processor for applying or removing borders to media assets."""
logger = Logger(config.log_level, config.module_name)
def no_assets_exit():
logger.info("\nNo assets found in the input directory")
logger.info("Please check the input directory and try again.")
logger.info("Exiting...")
return
log_dir = get_log_dir(config.module_name)
last_run = load_last_run(log_dir, logger=logger)
if config.log_level.lower() == "debug":
print_settings(logger, config)
# Set key variables from config or renamerr_config
merged = renamerr_config if renamerr_config else config
dry_run = getattr(merged, "dry_run", False)
destination_dir = getattr(merged, "destination_dir", None)
if not os.path.exists(destination_dir):
logger.error(f"Output directory {destination_dir} does not exist.")
return
# Get border colors and schedule
border_colors = None
run_holiday = False
switch = {"start_since_last_run": False, "end_since_last_run": False}
if getattr(config, "holidays", None):
run_holiday, border_colors, switch = check_holiday(
config, logger, last_run=last_run
)
if not border_colors:
border_colors = getattr(config, "border_colors", None)
# Get and group assets
def group_assets(assets):
if isinstance(assets, list):
grouped = {}
for asset in assets:
asset_type = asset.get("type")
grouped.setdefault(asset_type, []).append(asset)
return grouped
return assets
if (
renamed_assets is None
or switch["start_since_last_run"]
or switch["end_since_last_run"]
):
assets_dict, _ = get_assets_files(source_dirs, logger)
assets_dict = group_assets(assets_dict)
elif renamed_assets and incremental_run:
assets_dict = group_assets(renamed_assets)
else:
assets_dict = None
if not assets_dict or not any(assets_dict.values()):
return no_assets_exit()
# Just copy files if not scheduled to run
if not run_holiday and getattr(config, "skip", False):
messages = copy_files(assets_dict, destination_dir, dry_run, logger)
logger.info(
f"Skipping {config.module_name} as it is not scheduled to run today."
)
if messages:
logger.info(create_table([["Processed Files", f"{len(messages)}"]]))
for message in messages:
logger.info(message)
return
if destination_dir.endswith("/"):
destination_dir = destination_dir[:-1]
# Border logic
if not border_colors:
border_args = None
action_label = "removed border"
logger.info("No border colors set, removing borders from assets.")
logger.info("Executing border removal mode (removing all borders from assets).")
else:
border_args = border_colors
action_label = "replaced border"
logger.info(f"Using {', '.join(border_colors)} border color(s).")
logger.info(
"Executing border replacement mode (replacing borders with configured colors)."
)
messages = fix_borders(
assets_dict,
config,
border_args,
destination_dir,
dry_run,
logger,
getattr(config, "exclusion_list", None),
)
if messages:
logger.info(create_table([["Processed Files", f"{len(messages)}"]]))
logger.info(print_output(assets_dict, action_label, dry_run))
else:
logger.info("\nNo files processed")
if config.log_level == "debug":
print_json(assets_dict, logger, config.module_name, "assets_dict")
print_json(messages, logger, config.module_name, "messages")
save_last_run(log_dir, logger=logger)
# --- Formatting function for border actions output ---
def print_output(
assets_dict: Dict[str, List[Dict[str, Any]]], action: str, dry_run: bool
) -> str:
"""
Groups output by asset title and lists all processed files per asset.
Args:
assets_dict: Dictionary grouped by type, each with a list of asset dicts.
action: 'Removed border' or 'Replaced border'.
dry_run: If True, prefix with 'Would have'.
Returns:
str: Formatted output for logging or CLI.
"""
output_lines = []
for asset_type, assets in assets_dict.items():
for asset in assets:
title = asset.get("title")
year = asset.get("year")
if year:
display = f"{title} ({year})"
else:
display = f"{title}"
prefix = f"Would have {action} on" if dry_run else f"{action} on"
output_lines.append(f"{prefix} '{display}'")
files = asset.get("files", [])
for file_path in files:
file_name = os.path.basename(file_path)
output_lines.append(f" {file_name}")
return "\n".join(output_lines)
def main(config: SimpleNamespace) -> None:
"""
Entry point for running the border replacer module.
Args:
config (SimpleNamespace): Main configuration object.
"""
logger = Logger(config.log_level, config.module_name)
try:
process_files(
config.source_dirs,
config,
logger=logger,
renamed_assets=None,
renamerr_config=None,
)
except KeyboardInterrupt:
print("Keyboard Interrupt detected. Exiting...")
sys.exit()
except Exception:
logger.error("\n\nAn error occurred:\n", exc_info=True)
logger.error("\n\n")
finally:
# Log outro message with run time
logger.log_outro()
================================================
FILE: modules/health_checkarr.py
================================================
import json
import re
import sys
from types import SimpleNamespace
from typing import Any, Dict, List, Optional
from tqdm import tqdm
from util.arrpy import create_arr_client
from util.constants import tmdb_id_regex, tvdb_id_regex
from util.logger import Logger
from util.notification import send_notification
from util.utility import create_table, print_settings, progress
def main(config: SimpleNamespace) -> None:
"""
Process Radarr and Sonarr instances to identify and delete media items flagged by health checks
as removed from TMDB or TVDB. Supports dry run mode and logs all actions.
Args:
config (SimpleNamespace): Configuration object containing:
- log_level (str): Logging verbosity level.
- module_name (str): Name of the module for logging context.
- dry_run (bool): If True, no deletions are performed.
- instances_config (Dict[str, Dict[str, Dict[str, str]]]): Configuration for each instance type and instance.
- instances (List[str]): List of instance names to process.
Behavior:
- Iterates over configured Radarr and Sonarr instances.
- Retrieves health check data and media libraries.
- Identifies media items flagged as removed.
- Deletes flagged media items unless dry run is enabled.
- Sends notifications about deleted items.
- Logs all key steps and errors.
"""
logger: Logger = Logger(config.log_level, config.module_name)
try:
# Display configuration settings if in debug mode
if config.log_level.lower() == "debug":
print_settings(logger, config)
# Print dry run notice if enabled
if config.dry_run:
table: List[List[str]] = [["Dry Run"], ["NO CHANGES WILL BE MADE"]]
logger.info(create_table(table))
logger.info("")
# Iterate over each instance type (radarr/sonarr)
for instance_type, instance_data in config.instances_config.items():
# Iterate over each configured instance for this type
for instance in config.instances:
if instance in instance_data:
# Create client for the current instance
app: Optional[Any] = create_arr_client(
instance_data[instance]["url"],
instance_data[instance]["api"],
logger,
)
if app and app.connect_status:
# Retrieve health check warnings
health: Optional[List[Dict[str, Any]]] = app.get_health()
# Retrieve current media library without episode details
media_dict: List[Dict[str, Any]] = app.get_parsed_media(
include_episode=False
)
id_list: List[int] = []
# Parse health check messages for removed media IDs
if health:
for health_item in health:
if (
health_item["source"] == "RemovedMovieCheck"
or health_item["source"] == "RemovedSeriesCheck"
):
if instance_type == "radarr":
for m in re.finditer(
tmdb_id_regex, health_item["message"]
):
id_list.append(int(m.group(1)))
if instance_type == "sonarr":
for m in re.finditer(
tvdb_id_regex, health_item["message"]
):
id_list.append(int(m.group(1)))
logger.debug(f"id_list:\n{json.dumps(id_list, indent=4)}")
output: List[Dict[str, Any]] = []
# Match health-check IDs with media library entries
with progress(
media_dict,
desc=f"Processing {instance_type}",
unit="items",
logger=logger,
leave=True,
) as pbar:
for item in pbar:
if (
instance_type == "radarr"
and item["tmdb_id"] in id_list
) or (
instance_type == "sonarr"
and item["tvdb_id"] in id_list
):
db_id = (
item["tmdb_id"]
if instance_type == "radarr"
else item["tvdb_id"]
)
logger.debug(
f"Found {item['title']} with: {db_id}"
)
output.append(item)
logger.debug(f"output:\n{json.dumps(output, indent=4)}")
if output:
logger.info(
f"Deleting {len(output)} {instance_type} items from {app.instance_name}"
)
# Delete each matched item unless dry run is enabled
for item in tqdm(
output,
desc=f"Deleting {instance_type} items",
unit="items",
disable=None,
total=len(output),
):
if not config.dry_run:
logger.info(
f"{item['title']} deleted with id: {item['media_id']} and tvdb/tmdb id: {item['db_id']}"
)
app.delete_media(item["media_id"])
else:
logger.info(
f"{item['title']} would have been deleted with id: {item['media_id']}"
)
# Send notification with deleted items
send_notification(
logger=logger,
module_name=config.module_name,
config=config,
output=output,
)
else:
logger.info(
f"No health data returned for {app.instance_name}, this is fine if there was nothing to delete. Skipping deletion checks."
)
except KeyboardInterrupt:
print("Keyboard Interrupt detected. Exiting...")
sys.exit()
except Exception:
logger.error("\n\nAn error occurred:\n", exc_info=True)
logger.error("\n\n")
finally:
# Log outro message with run time
logger.log_outro()
================================================
FILE: modules/jduparr.py
================================================
import os
import subprocess
import sys
from types import SimpleNamespace
from util.logger import Logger
from util.notification import send_notification
from util.utility import create_table, print_settings
def print_output(output: list[dict], logger: Logger) -> None:
"""
Print the results of the duplicate file search and linking process.
Args:
output (list[dict]): List of dictionaries containing path, message, files, and counts.
logger (Logger): Logger instance to output messages.
"""
count = 0
for item in output:
path = item.get("source_dir")
field_message = item.get("field_message")
files = item.get("output")
sub_count = item.get("sub_count")
logger.info(f"Findings for path: {path}")
logger.info(f"\t{field_message}")
for i in files:
count += 1
logger.info(f"\t\t{i}")
count += sub_count
logger.info(
f"\tTotal items for '{os.path.basename(os.path.normpath(path))}': {sub_count}"
)
logger.info(f"Total items relinked: {count}")
def main(config: SimpleNamespace) -> None:
"""
Main execution function for identifying and hardlinking duplicate media files using jdupes.
Args:
config (SimpleNamespace): Configuration object containing source directories, logging, and other settings.
Returns:
None
"""
logger = Logger(config.log_level, config.module_name)
results = None
try:
# If dry run, display a notice table
if config.dry_run:
table = [["Dry Run"], ["NO CHANGES WILL BE MADE"]]
logger.info(create_table(table))
output = []
# Iterate over each source directory to find duplicates
if not config.source_dirs:
logger.error(
f"No source directories provided in config: {config.source_dirs}"
)
return
for path in config.source_dirs:
if config.log_level.lower() == "debug":
print_settings(logger, config)
if not os.path.isdir(path):
logger.error(f"ERROR: path does not exist: {path}")
return
# Run jdupes to find duplicate media files with specified extensions
result = subprocess.getoutput(
f"jdupes -r -M -X onlyext:mp4,mkv,avi '{path}' 2>/dev/null"
)
# If not dry run and duplicates found, hardlink duplicates
if not config.dry_run:
if "No duplicates found." not in result:
subprocess.run(
f"jdupes -r -L -X onlyext:mp4,mkv,avi '{path}' 2>/dev/null",
shell=True,
)
# Parse filenames from jdupes output
parsed_files = sorted(
set(line.split("/")[-1] for line in result.splitlines() if "/" in line)
)
field_message = (
"✅ No unlinked files discovered..."
if not parsed_files
else "❌ Unlinked files discovered..."
)
sub_count = len(parsed_files)
output_data = {
"source_dir": path,
"field_message": field_message,
"output": parsed_files,
"sub_count": sub_count,
}
output.append(output_data)
if results:
logger.debug(f"jdupes output: {result}")
logger.debug(f"Parsed log: {parsed_files}")
# Print summarized output and send notification
print_output(output, logger)
send_notification(logger, config.module_name, config, output)
except KeyboardInterrupt:
print("Keyboard Interrupt detected. Exiting...")
sys.exit()
except Exception:
logger.error("An error occurred:", exc_info=True)
finally:
# Log outro message with run time
logger.log_outro()
================================================
FILE: modules/labelarr.py
================================================
import sys
from collections import defaultdict
from types import SimpleNamespace
from typing import Dict, List, Optional
from util.arrpy import BaseARRClient, create_arr_client
from util.logger import Logger
from util.normalization import normalize_titles
from util.notification import send_notification
from util.utility import create_table, print_settings, progress
try:
from plexapi.exceptions import BadRequest
from plexapi.server import PlexServer
except ImportError as e:
print(f"ImportError: {e}")
print("Please install the required modules with 'pip install -r requirements.txt'")
exit(1)
def sync_to_plex(
plex: PlexServer,
labels: List[str],
media_dict: List[Dict],
app: BaseARRClient,
logger: Logger,
library_names: List[str],
config: SimpleNamespace,
) -> List[Dict]:
"""
Synchronize label metadata between an ARR client and Plex libraries.
Args:
plex (PlexServer): Plex server instance.
labels (List[str]): List of label names to sync.
media_dict (List[Dict]): List of media entries from ARR.
app (BaseARRClient): ARR client instance (Radarr or Sonarr).
logger (Logger): Logger instance.
library_names (List[str]): Names of Plex libraries to process.
config (SimpleNamespace): Configuration object.
Returns:
List[Dict]: List of label changes applied or identified.
"""
tag_ids: Dict[str, Optional[int]] = {}
for label in labels:
tag_id = app.get_tag_id_from_name(label)
if tag_id:
tag_ids[label] = tag_id
# Create lookups
tmdb_imdb_lookup = {
(media.get("tmdb_id"), media.get("imdb_id")): media
for media in media_dict
if media.get("tmdb_id") is not None and media.get("imdb_id")
}
tvdb_imdb_lookup = {
(media.get("tvdb_id"), media.get("imdb_id")): media
for media in media_dict
if media.get("tvdb_id") is not None and media.get("imdb_id")
}
tmdb_lookup = {
media["tmdb_id"]: media
for media in media_dict
if media.get("tmdb_id") is not None
}
tvdb_lookup = {
media["tvdb_id"]: media
for media in media_dict
if media.get("tvdb_id") is not None
}
imdb_lookup = {
media["imdb_id"]: media
for media in media_dict
if media.get("imdb_id") is not None
}
fallback_lookup = {
(media["normalized_title"], media["year"]): media
for media in media_dict
if "normalized_title" in media and "year" in media
}
data_dict: List[Dict] = []
with progress(
library_names,
desc="Processing Libraries",
unit="items",
logger=logger,
leave=True,
) as outer_pbar:
for library in outer_pbar:
library_data = plex.library.section(library).all()
with progress(
library_data,
desc=f"Syncing labels between {app.instance_name.capitalize()} and {library}",
unit="items",
logger=logger,
leave=True,
) as inner_pbar:
for library_item in inner_pbar:
try:
plex_item_labels = [
label.tag.lower() for label in library_item.labels
]
except AttributeError:
logger.error(
f"Error fetching labels for {getattr(library_item, 'title', str(library_item))} (no labels)"
)
continue
# Safely extract IDs
ids: Dict[str, Optional[str]] = {
"tmdb": None,
"tvdb": None,
"imdb": None,
}
for guid in getattr(library_item, "guids", []):
guid_str = getattr(guid, "id", "")
if guid_str.startswith("tmdb://"):
ids["tmdb"] = guid_str.split("tmdb://")[1]
elif guid_str.startswith("tvdb://"):
ids["tvdb"] = guid_str.split("tvdb://")[1]
elif guid_str.startswith("imdb://"):
ids["imdb"] = guid_str.split("imdb://")[1]
media_item: Optional[Dict] = None
match_type: str = "unknown"
# 1. Prefer TMDB+IMDB or TVDB+IMDB
tmdb_id = ids.get("tmdb")
imdb_id = ids.get("imdb")
tvdb_id = ids.get("tvdb")
# Prefer TMDB+IMDB
if tmdb_id and tmdb_id.isdigit() and imdb_id:
key = (int(tmdb_id), imdb_id)
media_item = tmdb_imdb_lookup.get(key)
if media_item:
match_type = f"TMDB+IMDB MATCH: TMDB {tmdb_id} & IMDB {imdb_id}"
# Next try TVDB+IMDB
if not media_item and tvdb_id and tvdb_id.isdigit() and imdb_id:
key = (int(tvdb_id), imdb_id)
media_item = tvdb_imdb_lookup.get(key)
if media_item:
match_type = f"TVDB+IMDB MATCH: TVDB {tvdb_id} & IMDB {imdb_id}"
# 2. Fallback to just TMDB, TVDB, IMDB
if not media_item and tmdb_id and tmdb_id.isdigit():
media_item = tmdb_lookup.get(int(tmdb_id))
if media_item:
match_type = f"TMDB MATCH: {tmdb_id}"
if not media_item and tvdb_id and tvdb_id.isdigit():
media_item = tvdb_lookup.get(int(tvdb_id))
if media_item:
match_type = f"TVDB MATCH: {tvdb_id}"
if not media_item and imdb_id:
media_item = imdb_lookup.get(imdb_id)
if media_item:
match_type = f"IMDB MATCH: {imdb_id}"
# 3. Final fallback to normalized title and year
if not media_item:
norm_title = normalize_titles(getattr(library_item, "title", ""))
item_year = getattr(library_item, "year", None)
if item_year is not None:
key = (norm_title, item_year)
media_item = fallback_lookup.get(key)
if media_item:
match_type = "TITLE/YEAR MATCH"
else:
logger.debug(
f"Skipping fallback match for '{getattr(library_item, 'title', str(library_item))}' as no 'year' attribute is present (likely not a movie/show item)"
)
# 4. Only proceed if a real match was found
if media_item:
logger.debug(
f"Matched '{getattr(library_item, 'title', str(library_item))}' ({getattr(library_item, 'year', '-')}) using {match_type} to '{media_item.get('title', '-')}' ({media_item.get('year', '-')})"
)
add_remove: Dict[str, str] = {}
# Decide add/remove per label
for tag, id in tag_ids.items():
if tag not in plex_item_labels and id in media_item["tags"]:
add_remove[tag] = "add"
if not config.dry_run:
library_item.addLabel(tag)
elif (
tag in plex_item_labels and id not in media_item["tags"]
):
add_remove[tag] = "remove"
if not config.dry_run:
library_item.removeLabel(tag)
if add_remove:
data_dict.append(
{
"title": getattr(library_item, "title", str(library_item)),
"year": getattr(library_item, "year", None),
"add_remove": add_remove,
}
)
return data_dict
def handle_messages(data_dict: List[Dict], logger: Logger) -> None:
"""
Log label changes from sync results in a grouped, readable format.
Args:
data_dict (List[Dict]): List of dictionaries containing sync results.
logger (Logger): Logger instance for output.
"""
table: List[List[str]] = [["Results"]]
logger.info(create_table(table))
label_changes: Dict[tuple, List[str]] = defaultdict(list)
# Group media titles by label and action (add/remove)
for item in data_dict:
for label, action in item["add_remove"].items():
key = (label, action)
label_changes[key].append(f"{item['title']} ({item['year']})")
# Log grouped label changes
for (label, action), items in label_changes.items():
verb = "added to" if action == "add" else "removed from"
logger.info(f"\nLabel: {label} has been {verb}:")
for entry in items:
logger.info(f" - {entry}")
def main(config: SimpleNamespace) -> None:
"""
Main function to sync labels between Plex and Radarr/Sonarr based on configuration.
Args:
config (SimpleNamespace): Configuration object loaded from user settings.
"""
logger = Logger(config.log_level, config.module_name)
try:
# Print detailed settings if debug logging is enabled
if config.log_level.lower() == "debug":
print_settings(logger, config)
# Notify user if running in dry run mode (no actual changes will be made)
if config.dry_run:
table = [["Dry Run"], ["NO CHANGES WILL BE MADE"]]
logger.info(create_table(table))
output: List[Dict] = []
# Iterate over each mapping configured for syncing
for mapping in config.mappings:
app_type: str = mapping["app_type"]
app_instance: Optional[str] = mapping.get("app_instance")
labels: List[str] = mapping["labels"]
plex_instances: List[Dict] = mapping["plex_instances"]
app: Optional[BaseARRClient] = None
media_dict: List[Dict] = []
# Connect to the ARR client (Radarr or Sonarr) if specified
if app_type in ["radarr", "sonarr"] and app_instance:
app_config: Optional[Dict] = config.instances_config[app_type].get(
app_instance
)
if not app_config:
logger.error(
f"No config found for {app_type} instance '{app_instance}'"
)
continue
app = create_arr_client(app_config["url"], app_config["api"], logger)
if not app or not app.connect_status:
logger.error(
f"Failed to connect to {app_type} instance {app_instance}"
)
continue
# Fetch parsed media list from ARR client, excluding episodes for Sonarr
if (
hasattr(app, "instance_type")
and app.instance_type.lower() == "sonarr"
):
media_dict = app.get_parsed_media(include_episode=False)
else:
media_dict = app.get_parsed_media()
if not media_dict:
logger.info(f"No media found for {app_instance}")
continue
# Process each Plex instance and library associated with the mapping
if plex_instances:
for mapping_block in plex_instances:
plex_instance: Optional[str] = mapping_block.get("instance")
library_names: List[str] = mapping_block.get("library_names", [])
if plex_instance not in config.instances_config.get("plex", {}):
logger.error(
f"No Plex instance found for {plex_instance}. Skipping...\n"
)
continue
try:
plex = PlexServer(
config.instances_config["plex"][plex_instance]["url"],
config.instances_config["plex"][plex_instance]["api"],
timeout=180,
)
logger.info(f"Connected to Plex instance '{plex.friendlyName}'")
except BadRequest:
logger.error(
f"Error connecting to Plex instance: {plex_instance}"
)
continue
if library_names:
label_str = ", ".join(labels)
logger.info(
f"Syncing labels [{label_str}] from {app_type.capitalize()} instance '{app_instance}' to Plex instance '{plex_instance}'"
)
# Collect changes from sync_to_plex and accumulate in output list
data_dict = sync_to_plex(
plex, labels, media_dict, app, logger, library_names, config
)
output.extend(data_dict)
else:
logger.error(
f"No library names provided for {plex_instance}. Skipping..."
)
continue
# Log and send notifications if any label changes were found
if output:
handle_messages(output, logger)
# Only send notifications if not in dry run mode
send_notification(
logger=logger,
module_name=config.module_name,
config=config,
output=output,
)
else:
logger.info("No labels to sync to Plex")
except KeyboardInterrupt:
print("Keyboard Interrupt detected. Exiting...")
sys.exit()
except Exception:
logger.error("\n\nAn error occurred:\n", exc_info=True)
logger.error("\n\n")
finally:
# Log outro message with run time
logger.log_outro()
================================================
FILE: modules/nohl.py
================================================
import os
import re
import sys
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
from util.arrpy import create_arr_client
from util.constants import (
episode_regex,
season_regex,
year_regex,
)
from util.logger import Logger
from util.notification import send_notification
from util.utility import (
create_table,
normalize_titles,
print_json,
print_settings,
progress,
)
VIDEO_EXTS = (".mkv", ".mp4")
if TYPE_CHECKING:
from util.arrpy import BaseARRClient
def find_nohl_files(
path: str, logger: Logger
) -> Optional[Dict[str, List[Dict[str, Any]]]]:
"""
Find all video files in a directory tree that are not hardlinked.
Args:
path: Root directory to scan.
logger: Logger instance for debug output.
Returns:
Dictionary with non-hardlinked movies and series details.
"""
path_basename = os.path.basename(path.rstrip("/"))
nohl_data: Dict[str, List[Dict[str, Any]]] = {"movies": [], "series": []}
logger.debug(f"Scanning directory: {path}")
try:
entries = [i for i in os.listdir(path) if os.path.isdir(os.path.join(path, i))]
except FileNotFoundError as e:
logger.error(f"Error: {e}")
return None
for item in progress(
entries,
desc=f"Searching '{path_basename}'",
unit="item",
total=len(entries),
logger=logger,
):
if item.startswith("."):
continue
# Remove year from directory name for title
title = re.sub(year_regex, "", item)
try:
year = int(year_regex.search(item).group(1))
except AttributeError:
year = 0
asset_list: Dict[str, Any] = {
"title": title,
"year": year,
"normalized_title": normalize_titles(title),
"root_path": os.path.join(*path.rstrip(os.sep).split(os.sep)[-2:]),
"path": os.path.join(path, item),
}
item_path = os.path.join(path, item)
# Detect if this is a series (has subfolders) or a movie (just files)
if os.path.isdir(item_path) and any(
os.path.isdir(os.path.join(item_path, sub_folder))
for sub_folder in os.listdir(item_path)
):
sub_folders = [
sub_folder
for sub_folder in os.listdir(item_path)
if os.path.isdir(os.path.join(item_path, sub_folder))
and not sub_folder.startswith(".")
]
asset_list["season_info"] = []
for sub_folder in sub_folders:
sub_folder_path = os.path.join(item_path, sub_folder)
sub_folder_files = [
file
for file in os.listdir(sub_folder_path)
if os.path.isfile(os.path.join(sub_folder_path, file))
and not file.startswith(".")
]
season = re.search(season_regex, sub_folder)
try:
season_number = int(season.group(1))
except AttributeError:
season_number = 0
nohl_files = []
# Non-hardlink detection for each file in season folder
for file in sub_folder_files:
if not file.endswith(VIDEO_EXTS):
continue
file_path = os.path.join(sub_folder_path, file)
try:
st = os.stat(file_path)
if st.st_nlink == 1:
nohl_files.append(file_path)
except Exception:
continue
if nohl_files:
logger.debug(
f"Found {len(nohl_files)} non-hardlinked files in '{sub_folder_path}'"
)
episodes = []
# Extract episode numbers from non-hardlinked files
for file in nohl_files:
try:
episode_match = re.search(episode_regex, file)
if episode_match is not None:
episode = int(episode_match.group(1))
episodes.append(episode)
except Exception as e:
logger.error(f"{e}")
logger.error(f"Error processing file: {file}.")
continue
season_list = {
"season_number": season_number,
"episodes": episodes,
"nohl": nohl_files,
}
if nohl_files:
asset_list["season_info"].append(season_list)
# Only add if there are any non-hardlinked episodes in any season
if asset_list.get("season_info") and any(
season["nohl"] for season in asset_list["season_info"]
):
nohl_data["series"].append(asset_list)
else:
files_path = item_path
files = [
file
for file in os.listdir(files_path)
if os.path.isfile(os.path.join(files_path, file))
and not file.startswith(".")
]
nohl_files = []
# Non-hardlink detection for movie files
for file in files:
if not file.endswith(VIDEO_EXTS):
continue
file_path = os.path.join(files_path, file)
try:
st = os.stat(file_path)
if st.st_nlink == 1:
nohl_files.append(file_path)
except Exception:
continue
if nohl_files:
logger.debug(
f"Found {len(nohl_files)} non-hardlinked files in '{item_path}'"
)
asset_list["nohl"] = nohl_files
if nohl_files:
nohl_data["movies"].append(asset_list)
# Sort seasons and episodes numerically for each series
for series in nohl_data["series"]:
if "season_info" in series:
series["season_info"].sort(key=lambda s: int(s["season_number"]))
for season in series["season_info"]:
if "episodes" in season:
season["episodes"].sort(key=int)
return nohl_data
def handle_searches(
app: "BaseARRClient",
search_list: List[Dict[str, Any]],
instance_type: str,
logger: Logger,
config,
) -> List[Dict[str, Any]]:
"""
Perform search and deletion actions for Radarr or Sonarr items.
Args:
app: ARR API client.
search_list: List of media dicts to search.
instance_type: "radarr" or "sonarr".
logger: Logger instance.
config: Config object.
Returns:
List of items that were searched.
"""
logger.debug(f"Initiating search for {len(search_list)} items in {instance_type}")
print("Searching for files... this may take a while.")
searched_for: List[Dict[str, Any]] = []
searches = 0
for item in progress(
search_list,
desc="Searching...",
unit="item",
total=len(search_list),
logger=logger,
):
if instance_type == "radarr":
# Radarr: delete file(s) and trigger search for the movie
if config.dry_run:
logger.info(
f"[Dry Run] Would search: {item['title']} ({item['year']}) and delete file IDs: {item['file_ids']}"
)
searched_for.append(item)
searches += 1
else:
app.delete_movie_file(item["file_ids"])
results = app.refresh_items(item["media_id"])
ready = app.wait_for_command(results["id"])
if ready:
logger.debug(
f"Performing a Search for {item['media_id']} ({item['year']})"
)
app.search_media(item["media_id"])
searched_for.append(item)
searches += 1
logger.debug(f"Searched: {item['title']} ({item['year']})")
elif instance_type == "sonarr":
# Sonarr: for each season, trigger episode or season pack search
seasons = item.get("seasons", [])
if seasons:
for season in seasons:
season_pack = season["season_pack"]
file_ids = list(
set(
[
episode["episode_file_id"]
for episode in season["episode_data"]
]
)
)
episode_ids = [
episode["episode_id"] for episode in season["episode_data"]
]
if season_pack:
if config.dry_run:
logger.info(
f"[Dry Run] Would search season: {season['season_number']} of {item['title']} ({item['year']})"
)
else:
app.delete_episode_files(file_ids)
results = app.refresh_items(item["media_id"])
ready = app.wait_for_command(results["id"])
if ready:
logger.debug(
f"Performing a season search for {item['media_id']} ({item['year']}) Season Number: {season['season_number']}"
)
app.search_season(
item["media_id"], season["season_number"]
)
else:
if config.dry_run:
episode_numbers = [
ep["episode_number"] for ep in season["episode_data"]
]
logger.info(
f"[Dry Run] Would search episodes: {episode_numbers} of {item['title']} ({item['year']})"
)
else:
app.delete_episode_files(file_ids)
results = app.refresh_items(item["media_id"])
ready = app.wait_for_command(results["id"])
if ready:
logger.debug(
f"Performing an episode search for {item['title']} ({item['year']}), Episodes IDs: {episode_ids}"
)
app.search_episodes(episode_ids)
searched_for.append(item)
logger.debug(f"Searched: {item['title']} ({item['year']})")
print(f"Searches performed: {searches}")
return searched_for
def filter_media(
app: "BaseARRClient",
media_dict: List[Dict[str, Any]],
nohl_data: List[Dict[str, Any]],
instance_type: str,
config,
logger: Logger,
) -> Dict[str, List[Dict[str, Any]]]:
"""
Filter media to exclude items based on monitoring, exclusion lists, and quality profiles.
Args:
app: ARR client for Radarr/Sonarr.
media_dict: List of media items from ARR.
nohl_data: List of non-hardlinked media items.
instance_type: "radarr" or "sonarr".
config: Script configuration.
logger: Logger instance.
Returns:
Dict with 'search_media' and 'filtered_media' lists.
"""
logger.debug(
f"Filtering {len(nohl_data)} nohl items against {len(media_dict)} media items from {instance_type}"
)
quality_profiles = app.get_quality_profile_names()
exclude_profile_ids = []
if config.exclude_profiles:
for profile in config.exclude_profiles:
if profile in quality_profiles:
exclude_profile_ids.append(quality_profiles[profile])
def build_season_filtering(
media_season: Dict[str, Any], file_season: Dict[str, Any]
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
"""
Split a season into filtered (excluded) and search-needed episodes based on monitoring and matches.
"""
season_data: List[Dict[str, Any]] = []
filtered_seasons: List[Dict[str, Any]] = []
if not media_season["monitored"]:
# Unmonitored season, add to filtered
filtered_seasons.append(
{
"season_number": media_season["season_number"],
"monitored": False,
}
)
else:
if media_season["season_pack"]:
# Monitored season pack, add all to search
season_data.append(
{
"season_number": media_season["season_number"],
"season_pack": True,
"episode_data": media_season["episode_data"],
}
)
else:
# For non-season-pack, filter out unmonitored and select monitored matching episodes
episode_set = set(file_season["episodes"])
filtered_episodes = []
episode_data = []
for episode in media_season["episode_data"]:
if not episode["monitored"]:
# Unmonitored episode, add to filtered
filtered_episodes.append(episode)
elif episode["episode_number"] in episode_set:
# Monitored and present in file_season, add to search
episode_data.append(episode)
if filtered_episodes:
# Unmonitored chunk
filtered_seasons.append(
{
"season_number": media_season["season_number"],
"monitored": True,
"episodes": filtered_episodes,
}
)
if episode_data:
# Monitored chunk that needs searching
season_data.append(
{
"season_number": media_season["season_number"],
"season_pack": False,
"episode_data": episode_data,
}
)
return season_data, filtered_seasons
data_list: Dict[str, List[Dict[str, Any]]] = {
"search_media": [],
"filtered_media": [],
}
for nohl_item in progress(
nohl_data,
desc="Filtering media...",
unit="item",
total=len(nohl_data),
logger=logger,
):
for media_item in media_dict:
# ARR resolution: match normalized title and year
if (
media_item["normalized_title"] == nohl_item["normalized_title"]
and media_item["year"] == nohl_item["year"]
):
# Only match root_path if not dry_run
if (
nohl_item["root_path"] not in media_item["root_folder"]
and not config.dry_run
):
logger.debug(
f"Skipping {media_item['title']} ({media_item['year']}), root folder mismatch."
)
continue
# Exclusion checks: monitored, exclusion lists, quality profile
if (
media_item["monitored"] is False
or (
instance_type == "radarr"
and config.exclude_movies
and media_item["title"] in config.exclude_movies
)
or (
instance_type == "sonarr"
and config.exclude_series
and media_item["title"] in config.exclude_series
)
or media_item["quality_profile"] in exclude_profile_ids
):
data_list["filtered_media"].append(
{
"title": media_item["title"],
"year": media_item["year"],
"monitored": media_item["monitored"],
"excluded": (
(
instance_type == "radarr"
and config.exclude_movies
and media_item["title"] in config.exclude_movies
)
or (
instance_type == "sonarr"
and config.exclude_series
and media_item["title"] in config.exclude_series
)
),
"quality_profile": (
quality_profiles.get(media_item["quality_profile"])
if media_item["quality_profile"] in exclude_profile_ids
else None
),
}
)
logger.debug(
f"Filtered out: {media_item['title']} ({media_item['year']}), reason(s): "
f"{'not monitored' if media_item['monitored'] is False else ''}"
f"{', excluded' if (instance_type == 'radarr' and config.exclude_movies and media_item['title'] in config.exclude_movies) or (instance_type == 'sonarr' and config.exclude_series and media_item['title'] in config.exclude_series) else ''}"
f"{', quality profile' if media_item['quality_profile'] in exclude_profile_ids else ''}"
)
continue
if instance_type == "radarr":
# Add movie to search list
file_ids = media_item["file_id"]
data_list["search_media"].append(
{
"media_id": media_item["media_id"],
"title": media_item["title"],
"year": media_item["year"],
"file_ids": file_ids,
}
)
logger.debug(
f"Radarr: Will resolve {media_item['title']} ({media_item['year']}), file_ids={file_ids}"
)
elif instance_type == "sonarr":
# Season filtering for Sonarr: build per-season search/exclude lists
media_seasons_info = media_item.get("seasons", {})
file_season_info = nohl_item.get("season_info", [])
season_data = []
filtered_seasons = []
for media_season in media_seasons_info:
for file_season in file_season_info:
if (
media_season["season_number"]
== file_season["season_number"]
):
sdata, sfiltered = build_season_filtering(
media_season, file_season
)
season_data.extend(sdata)
filtered_seasons.extend(sfiltered)
if filtered_seasons:
data_list["filtered_media"].append(
{
"title": media_item["title"],
"year": media_item["year"],
"seasons": filtered_seasons,
}
)
logger.debug(
f"Filtered out: {media_item['title']} ({media_item['year']}), reason(s): "
f"{'not monitored' if media_item['monitored'] is False else ''}"
f"{', excluded' if (instance_type == 'radarr' and config.exclude_movies and media_item['title'] in config.exclude_movies) or (instance_type == 'sonarr' and config.exclude_series and media_item['title'] in config.exclude_series) else ''}"
f"{', quality profile' if media_item['quality_profile'] in exclude_profile_ids else ''}"
)
if season_data:
logger.debug(
f"{media_item['title']} ({media_item['year']}): {len(season_data)} seasons selected for search"
)
data_list["search_media"].append(
{
"media_id": media_item["media_id"],
"title": media_item["title"],
"year": media_item["year"],
"monitored": media_item["monitored"],
"seasons": season_data,
}
)
logger.debug(
f"Sonarr: Will resolve {media_item['title']} ({media_item['year']}), seasons: "
f"{[s['season_number'] for s in season_data]}"
)
# Limit number of searches if configured
if len(data_list["search_media"]) >= config.searches:
data_list["search_media"] = data_list["search_media"][: config.searches]
return data_list
def handle_messages(output: Dict[str, Any], logger: Logger) -> None:
"""
Print a formatted summary of scanned non-hardlinked files and resolved ARR actions.
Args:
output: Output dictionary containing scan and resolve results.
logger: Logger instance.
"""
# Output scanned section: show all non-hardlinked movies and series found
logger.info(create_table([["Scanned Non-Hardlinked Files"]]))
for path, results in output.get("scanned", {}).items():
logger.info(f"Scanning results for: {path}")
for item in results.get("movies", []):
logger.info(f"{item['title']} ({item['year']})")
if item.get("nohl"):
for file_path in item["nohl"]:
logger.info(f"\t{os.path.basename(file_path)}")
logger.info("")
for item in results.get("series", []):
logger.info(f"{item['title']} ({item['year']})")
for season in item.get("season_info", []):
if season.get("nohl"):
logger.info(f"\tSeason {season['season_number']}")
for file_path in season["nohl"]:
logger.info(f"\t\t{os.path.basename(file_path)}")
logger.info("")
# Output resolved section: show all ARR actions performed or skipped
logger.info(create_table([["Resolved ARR Actions"]]))
for instance, instance_data in output.get("resolved", {}).items():
search_media = instance_data["data"]["search_media"]
filtered_media = instance_data["data"]["filtered_media"]
# Output searched ARR media
if search_media:
for search_item in search_media:
if instance_data["instance_type"] == "radarr":
logger.info(f"{search_item['title']} ({search_item['year']})")
logger.info("\tDeleted and searched.\n")
else:
logger.info(f"{search_item['title']} ({search_item['year']})")
if search_item.get("seasons", None):
for season in search_item["seasons"]:
if season["season_pack"]:
logger.info(
f"\tSeason {season['season_number']}, deleted and searched."
)
else:
logger.info(f"\tSeason {season['season_number']}")
for episode in season["episode_data"]:
logger.info(
f"\t Episode {episode['episode_number']}, deleted and searched."
)
logger.info("")
# Output filtered ARR media (excluded or unmonitored)
table = [["Filtered Media"]]
if filtered_media:
logger.debug(create_table(table))
for filtered_item in filtered_media:
monitored = filtered_item.get("monitored", None)
logger.debug(f"{filtered_item['title']} ({filtered_item['year']})")
if monitored is False:
logger.debug("\tSkipping, not monitored.")
elif filtered_item.get("exclude_media", None):
logger.debug("\tSkipping, excluded.")
elif filtered_item.get("quality_profile", None):
logger.debug(
f"\tSkipping, quality profile: {filtered_item['quality_profile']}"
)
elif filtered_item.get("seasons", None):
for season in filtered_item["seasons"]:
if season["monitored"] is False:
logger.debug(
f"\tSeason {season['season_number']}, skipping, not monitored."
)
elif season.get("episodes", None):
logger.debug(f"\tSeason {season['season_number']}")
for episode in season["episodes"]:
logger.debug(
f"\t Episode {episode['episode_number']}, skipping, not monitored."
)
logger.debug("")
else:
logger.debug(f"No filtered files for {instance_data['server_name']}")
logger.debug("")
# Output summary table
summary = output.get("summary", {})
if not all(value == 0 for value in summary.values()):
logger.info(
create_table(
[
["Metric", "Count"],
["Total Scanned Movies", summary.get("total_scanned_movies", 0)],
["Total Scanned Episodes", summary.get("total_scanned_series", 0)],
["Total Resolved Movies", summary.get("total_resolved_movies", 0)],
[
"Total Resolved Episodes",
summary.get("total_resolved_series", 0),
],
]
)
)
else:
logger.info("\n\n\t\t✅ Congratulations, there is nothing to report.\n\n")
def main(config) -> None:
"""
Entrypoint for nohl.py. Scans for non-hardlinked files and triggers ARR actions.
Args:
config: Parsed configuration namespace.
"""
logger = Logger(config.log_level, config.module_name)
try:
if config.log_level.lower() == "debug":
print_settings(logger, config)
# Warn if running in dry run mode
if config.dry_run:
table = [["Dry Run"], ["NO CHANGES WILL BE MADE"]]
logger.info(create_table(table))
logger.debug("Logger initialized. Starting main process.")
# Ensure ARR instances are configured
if config.instances is None:
logger.error("No instances set in config file.")
return
# Parse source_dirs into entries with path+mode
source_entries = []
if getattr(config, "source_dirs", None):
for entry in config.source_dirs:
if isinstance(entry, dict):
if "mode" not in entry:
logger.warning(f"No 'mode' set for source_dir path '{entry.get('path')}', defaulting to 'scan'.")
source_entries.append(
{
"path": entry.get("path"),
"mode": entry.get("mode", "scan"),
}
)
else:
source_entries.append({"path": entry, "mode": "resolve"})
# Separate scan vs resolve entries
scan_entries = [e for e in source_entries if e["mode"] == "scan"]
resolve_entries = [e for e in source_entries if e["mode"] == "resolve"]
# Scan-only: gather all non-hardlinked files for reporting
scanned_results: Dict[str, Any] = {}
for entry in scan_entries:
path = entry["path"]
results = find_nohl_files(path, logger)
scanned_results[path] = results or {"movies": [], "series": []}
# Resolve-only: aggregate all nohl results for ARR resolution
nohl_list: Dict[str, List[Dict[str, Any]]] = {"movies": [], "series": []}
for entry in resolve_entries:
path = entry["path"]
results = find_nohl_files(path, logger) or {"movies": [], "series": []}
if results and (results.get("movies") or results.get("series")):
nohl_list["movies"].extend(results.get("movies", []))
nohl_list["series"].extend(results.get("series", []))
else:
logger.warning(
f"No non-hardlinked files found in {path}, skipping resolution for this path"
)
continue
# Compute summary statistics for output reporting
total_movies = sum(
len(movie.get("nohl", []))
for results in scanned_results.values()
for movie in results.get("movies", [])
)
total_series = sum(
sum(len(season.get("nohl", [])) for season in series.get("season_info", []))
for results in scanned_results.values()
for series in results.get("series", [])
)
total_nohl_movies = sum(
len(movie.get("nohl", [])) for movie in nohl_list["movies"]
)
total_nohl_series = sum(
sum(len(season.get("nohl", [])) for season in series.get("season_info", []))
for series in nohl_list["series"]
)
total_scanned_movies = sum(
len(movie.get("nohl", []))
for path, results in scanned_results.items()
for movie in results.get("movies", [])
)
total_scanned_series = sum(
sum(len(season.get("nohl", [])) for season in series.get("season_info", []))
for path, results in scanned_results.items()
for series in results.get("series", [])
)
logger.debug(f"Total scanned movie files: {total_movies}")
logger.debug(f"Total scanned series files: {total_series}")
logger.debug(f"Total non-hardlinked movie files: {total_nohl_movies}")
logger.debug(f"Total non-hardlinked series files: {total_nohl_series}")
logger.debug(f"Total scanned results - movies: {total_scanned_movies}")
logger.debug(f"Total scanned results - series: {total_scanned_series}")
output_dict: Dict[str, Any] = {}
data_list: Dict[str, Any] = {}
media_dict: Any = {}
nohl_data: Any = {}
# ARR resolution: for each instance, filter and trigger searches
for instance_type, instance_data in config.instances_config.items():
for instance in config.instances:
if isinstance(instance, dict):
instance_name = next(iter(instance.keys()))
else:
instance_name = instance
if instance_name in instance_data:
data_list = {"search_media": [], "filtered_media": []}
instance_settings = instance_data.get(instance, None)
app = create_arr_client(
instance_settings["url"], instance_settings["api"], logger
)
if app and app.connect_status:
server_name = app.get_instance_name()
table = [[f"{server_name}"]]
logger.info(create_table(table))
if (instance_type == "radarr" and not nohl_list["movies"]) or (
instance_type == "sonarr" and not nohl_list["series"]
):
logger.info(
f"No non-hardlinked files found for server: {server_name}"
)
nohl_data = (
nohl_list["movies"]
if instance_type == "radarr"
else (
nohl_list["series"]
if instance_type == "sonarr"
else None
)
)
if nohl_data:
# Pull all media from ARR and filter for resolution
if instance_type == "sonarr":
media_dict = app.get_parsed_media(include_episode=True)
else:
media_dict = app.get_parsed_media()
if media_dict:
data_list = filter_media(
app,
media_dict,
nohl_data,
instance_type,
config,
logger,
)
else:
logger.info(f"No media found for server: {server_name}")
search_list = data_list.get("search_media", [])
if search_list:
# Conduct searches, with dry run support
search_list = handle_searches(
app, search_list, instance_type, logger, config
)
data_list["search_media"] = search_list
output_dict[instance] = {
"server_name": server_name,
"instance_type": instance_type,
"data": data_list,
}
logger.debug(
f"{server_name} processing complete. Search media: {len(data_list['search_media'])}, Filtered: {len(data_list['filtered_media'])}"
)
# Dump debug JSON payloads if needed
if config.log_level == "debug":
print_json(data_list, logger, config.module_name, "data_list")
print_json(media_dict, logger, config.module_name, "media_dict")
print_json(nohl_data, logger, config.module_name, "nohl_data")
print_json(output_dict, logger, config.module_name, "output_dict")
# Prepare summary for output reporting (only count actual resolved items in search_media)
resolved_movies = 0
resolved_episodes = 0
for instance, instance_data in output_dict.items():
search_media = instance_data["data"].get("search_media", [])
if instance_data["instance_type"] == "radarr":
resolved_movies += len(search_media)
elif instance_data["instance_type"] == "sonarr":
for search_item in search_media:
# Only count episodes in search_media (i.e., actually resolved)
if "seasons" in search_item:
for season in search_item["seasons"]:
resolved_episodes += len(season.get("episode_data", []))
summary = {
"total_scanned_movies": total_scanned_movies,
"total_scanned_series": total_scanned_series,
"total_resolved_movies": resolved_movies,
"total_resolved_series": resolved_episodes,
}
# Combine scan and resolve results for reporting and notification
final_output = {
"scanned": scanned_results,
"resolved": output_dict,
"summary": summary,
}
# Output results to console/log
handle_messages(final_output, logger)
# Send notification with scan+resolve results
send_notification(
logger=logger,
module_name=config.module_name,
config=config,
output=final_output,
)
except KeyboardInterrupt:
print("Keyboard Interrupt detected. Exiting...")
sys.exit()
except Exception:
logger.error("\n\nAn error occurred:\n", exc_info=True)
logger.error("\n\n")
finally:
# Log outro message with run time
logger.log_outro()
================================================
FILE: modules/poster_cleanarr.py
================================================
import os
import shutil
import sys
from types import SimpleNamespace
from typing import Dict, List, Optional, Union
from util.arrpy import create_arr_client
from util.assets import get_assets_files
from util.index import create_new_empty_index
from util.logger import Logger
from util.match import match_assets_to_media
from util.utility import (
create_table,
get_plex_data,
print_json,
print_settings,
)
try:
from plexapi.server import PlexServer
except ImportError as e:
print(f"ImportError: {e}")
print("Please install the required modules with 'pip install -r requirements.txt'")
exit(1)
def remove_assets(
unmatched_dict: List[Dict[str, Union[str, int, List[str], None]]],
config: SimpleNamespace,
logger: Logger,
) -> List[Dict[str, Union[str, int, List[str]]]]:
"""
Remove unmatched assets from disk or simulate removal.
Args:
unmatched_dict: List of unmatched asset dictionaries.
config: Configuration namespace.
logger: Logger instance.
Returns:
List of dictionaries summarizing removed assets and messages.
"""
remove_data: List[Dict[str, Union[str, int, List[str]]]] = []
remove_list: List[str] = []
# If input is a dict by type, flatten to a list
if isinstance(unmatched_dict, dict):
all_unmatched = []
for v in unmatched_dict.values():
all_unmatched.extend(v)
unmatched_list = all_unmatched
else:
unmatched_list = unmatched_dict
for asset_data in unmatched_list:
messages: List[str] = []
if not asset_data["files"] and asset_data.get("path"):
# Remove empty folder asset
remove_list.append(asset_data["path"])
messages.append(
f"Removing empty folder: {os.path.basename(asset_data['path'])}"
)
else:
# Remove individual files for asset
for file in asset_data["files"]:
remove_list.append(file)
# Compose tmp path
asset_dir = os.path.dirname(file)
basename = os.path.basename(file)
tmp_path = os.path.join(asset_dir, "tmp", basename)
if os.path.isfile(tmp_path):
remove_list.append(tmp_path)
if config.log_level.lower() == "debug":
messages.append(f"Removing duplicate in tmp: {tmp_path}")
messages.append(f"Removing file: {basename}")
remove_data.append(
{
"title": asset_data["title"],
"year": asset_data["year"],
"messages": messages,
}
)
if not config.dry_run:
for path in remove_list:
try:
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
# Remove parent folder if empty after file removal
folder_path = os.path.dirname(path)
if not os.listdir(folder_path):
os.rmdir(folder_path)
except OSError as e:
logger.error(f"Error: {e}")
logger.error(f"Failed to remove: {path}")
continue
# Clean up any remaining empty folders in source directories
for assets_path in config.source_dirs:
for root, dirs, files in os.walk(assets_path, topdown=False):
for dir_name in dirs:
dir_path = os.path.join(root, dir_name)
if not os.listdir(dir_path):
try:
logger.info(f"Removing empty folder: {dir_name}")
os.rmdir(dir_path)
except OSError as e:
logger.error(f"Error: {e}")
logger.error(f"Failed to remove: {dir_path}")
continue
return remove_data
def print_output(
remove_data: List[Dict[str, Union[str, int, List[str]]]], logger: Logger
) -> None:
"""
Print summary of removed assets and messages.
Args:
remove_data: List of dictionaries with removal information.
logger: Logger instance.
"""
# Add a banner/table at the very top
table = [["Assets Removed Summary"]]
logger.info(create_table(table))
count: int = 0
for data in remove_data:
title: str = data["title"]
year: Optional[int] = data.get("year")
if year:
logger.info(f"• {title} ({year})")
else:
logger.info(f"• {title}")
asset_messages: List[str] = data["messages"]
for message in asset_messages:
logger.info(f" - {message}")
count += 1
logger.info(f"\nTotal number of assets removed: {count}")
def main(config: SimpleNamespace) -> None:
"""
Main function to load media, match assets, and remove unmatched assets.
Args:
config: Configuration namespace.
"""
logger = Logger(config.log_level, config.module_name)
remove_data = []
try:
if config.log_level.lower() == "debug":
print_settings(logger, config)
if config.dry_run:
table = [["Dry Run"], ["NO CHANGES WILL BE MADE"]]
logger.info(create_table(table))
# Load assets from source directories
prefix_index = create_new_empty_index()
assets_dict, prefix_index = get_assets_files(config.source_dirs, logger)
if not assets_dict:
logger.error(
f"No assets found in the source directories: {config.source_dirs}"
)
return
media_dict = {"movies": [], "series": [], "collections": []}
if not config.instances:
logger.error("No instances found. Exiting script.")
return
for instance in config.instances:
if isinstance(instance, dict):
instance_name, instance_settings = next(iter(instance.items()))
else:
instance_name = instance
instance_settings = {}
found = False
instance_type = None
instance_data = None
for itype, idata in config.instances_config.items():
if instance_name in idata:
found = True
instance_type = itype
instance_data = idata
break
if not found or instance_type is None or instance_data is None:
logger.warning(
f"Instance '{instance_name}' not found in config.instances_config. Skipping."
)
continue
if instance_type == "plex":
url = instance_data[instance_name]["url"]
api = instance_data[instance_name]["api"]
try:
app = PlexServer(url, api)
except Exception as e:
logger.error(f"Error connecting to Plex: {e}")
app = None
if app:
library_names = instance_settings.get("library_names", [])
if library_names:
logger.info("Fetching Plex collections...")
results = get_plex_data(
app,
library_names,
logger,
include_smart=True,
collections_only=True,
)
media_dict["collections"].extend(results)
else:
logger.warning(
f"No library names specified for Plex instance '{instance_name}'. Skipping."
)
else:
url = instance_data[instance_name]["url"]
api = instance_data[instance_name]["api"]
app = create_arr_client(url, api, logger)
if app and app.connect_status:
logger.info(f"Fetching {app.instance_name} data...")
results = app.get_parsed_media(include_episode=False)
if results:
if instance_type == "radarr":
media_dict["movies"].extend(results)
elif instance_type == "sonarr":
media_dict["series"].extend(results)
else:
logger.warning(
f"No {instance_type.capitalize()} data found for instance '{instance_name}'."
)
if not any(media_dict.values()):
logger.error(
"No media found. Check 'instances' setting in your config. Exiting."
)
return
if media_dict and prefix_index:
logger.info("Matching assets to media, please wait...")
unmatched_dict = match_assets_to_media(
media_dict,
prefix_index,
logger,
return_unmatched_assets=True,
config=config,
strict_folder_match=True,
)
if any(unmatched_dict.values()):
remove_data = remove_assets(unmatched_dict, config, logger)
if remove_data:
print_output(remove_data, logger)
else:
logger.info("✅ No assets needed to be removed. Everything is in sync!")
# Only dump debug JSON if we're in debug mode
if config.log_level.lower() == "debug":
logger.debug("Dumping debug data for assets/media/unmatched/remove_data.")
print_json(assets_dict, logger, config.module_name, "assets_dict")
print_json(media_dict, logger, config.module_name, "media_dict")
print_json(unmatched_dict, logger, config.module_name, "unmatched_dict")
print_json(remove_data, logger, config.module_name, "remove_data")
except KeyboardInterrupt:
print("Keyboard Interrupt detected. Exiting...")
sys.exit()
except Exception:
logger.error("\n\nAn error occurred:\n", exc_info=True)
logger.error("\n\n")
finally:
logger.log_outro()
================================================
FILE: modules/poster_renamerr.py
================================================
import copy
import filecmp
import os
import re
import shutil
import sys
from types import SimpleNamespace
from typing import Any, Dict, List, Tuple
from util.arrpy import create_arr_client
from util.assets import get_assets_files
from util.constants import year_regex
from util.index import create_new_empty_index
from util.logger import Logger
from util.match import match_assets_to_media
from util.notification import send_notification
from util.utility import (
create_table,
get_plex_data,
print_json,
print_settings,
)
try:
from pathvalidate im
gitextract_jvbd20pz/
├── .dockerignore
├── .github/
│ └── workflows/
│ ├── on-branch-create.yml
│ ├── on-branch-delete.yml
│ ├── on-commit.yml
│ └── version.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── VERSION
├── compose/
│ └── docker-compose.yml
├── jokes.txt
├── main.py
├── modules/
│ ├── __init__.py
│ ├── border_replacerr.py
│ ├── health_checkarr.py
│ ├── jduparr.py
│ ├── labelarr.py
│ ├── nohl.py
│ ├── poster_cleanarr.py
│ ├── poster_renamerr.py
│ ├── renameinatorr.py
│ ├── sync_gdrive.py
│ ├── unmatched_assets.py
│ └── upgradinatorr.py
├── requirements.txt
├── start.sh
├── util/
│ ├── __init__.py
│ ├── arrpy.py
│ ├── assets.py
│ ├── config.py
│ ├── constants.py
│ ├── construct.py
│ ├── extract.py
│ ├── index.py
│ ├── logger.py
│ ├── match.py
│ ├── normalization.py
│ ├── notification.py
│ ├── notification_formatting.py
│ ├── scanner.py
│ ├── scheduler.py
│ ├── template/
│ │ └── config_template.json
│ ├── utility.py
│ └── version.py
└── web/
├── server.py
├── static/
│ ├── css/
│ │ ├── base.css
│ │ ├── common.css
│ │ ├── index.css
│ │ ├── instances.css
│ │ ├── layout.css
│ │ ├── logs.css
│ │ ├── modals.css
│ │ ├── navigation.css
│ │ ├── notifications.css
│ │ ├── poster_search.css
│ │ ├── schedule.css
│ │ └── settings.css
│ └── js/
│ ├── common.js
│ ├── help_content.js
│ ├── helper.js
│ ├── index.js
│ ├── instances.js
│ ├── logs.js
│ ├── main.js
│ ├── navigation.js
│ ├── notifications.js
│ ├── payload.js
│ ├── poster_search.js
│ ├── schedule.js
│ ├── settings/
│ │ ├── constants.js
│ │ ├── modal_helpers.js
│ │ ├── modals.js
│ │ ├── modules/
│ │ │ ├── border_replacerr.js
│ │ │ ├── health_checkarr.js
│ │ │ ├── jduparr.js
│ │ │ ├── labelarr.js
│ │ │ ├── main.js
│ │ │ ├── nohl.js
│ │ │ ├── poster_cleanarr.js
│ │ │ ├── poster_renamerr.js
│ │ │ ├── renameinatorr.js
│ │ │ ├── sync_gdrive.js
│ │ │ ├── unmatched_assets.js
│ │ │ └── upgradinatorr.js
│ │ ├── presets.js
│ │ └── settings_helpers.js
│ └── settings.js
└── templates/
├── index.html
└── pages/
├── instances.html
├── logs.html
├── notifications.html
├── poster_search.html
├── schedule.html
└── settings.html
SYMBOL INDEX (346 symbols across 56 files)
FILE: main.py
class ScheduleFileHandler (line 35) | class ScheduleFileHandler(FileSystemEventHandler):
method __init__ (line 36) | def __init__(self, callback, debounce_interval=1):
method on_modified (line 42) | def on_modified(self, event):
function start_schedule_watcher (line 51) | def start_schedule_watcher(callback):
class ModuleManager (line 64) | class ModuleManager:
method __init__ (line 65) | def __init__(self, logger):
method run (line 71) | def run(self, module_name, run_module):
method run_if_due (line 77) | def run_if_due(self, module_name, schedule_time, check_schedule_func, ...
method is_already_running (line 91) | def is_already_running(self, module_name):
method cleanup (line 94) | def cleanup(self):
method has_running_modules (line 107) | def has_running_modules(self):
function load_schedule (line 111) | def load_schedule():
function run_module (line 118) | def run_module(module_to_run, output=False, logger=None):
function print_schedule (line 132) | def print_schedule(logger, modules_schedules):
function main (line 143) | def main():
FILE: modules/border_replacerr.py
function load_last_run (line 24) | def load_last_run(log_dir: str, logger: Logger = None) -> Optional[datet...
function save_last_run (line 43) | def save_last_run(log_dir: str, dt: Optional[datetime] = None, logger: L...
function check_holiday (line 56) | def check_holiday(
function convert_to_rgb (line 120) | def convert_to_rgb(hex_color: str, logger: Logger) -> Tuple[int, int, int]:
function fix_borders (line 144) | def fix_borders(
function replace_borders (line 267) | def replace_borders(
function remove_borders (line 338) | def remove_borders(
function copy_files (line 414) | def copy_files(
function process_files (line 508) | def process_files(
function print_output (line 625) | def print_output(
function main (line 657) | def main(config: SimpleNamespace) -> None:
FILE: modules/health_checkarr.py
function main (line 16) | def main(config: SimpleNamespace) -> None:
FILE: modules/jduparr.py
function print_output (line 11) | def print_output(output: list[dict], logger: Logger) -> None:
function main (line 38) | def main(config: SimpleNamespace) -> None:
FILE: modules/labelarr.py
function sync_to_plex (line 21) | def sync_to_plex(
function handle_messages (line 211) | def handle_messages(data_dict: List[Dict], logger: Logger) -> None:
function main (line 239) | def main(config: SimpleNamespace) -> None:
FILE: modules/nohl.py
function find_nohl_files (line 29) | def find_nohl_files(
function handle_searches (line 173) | def handle_searches(
function filter_media (line 278) | def filter_media(
function handle_messages (line 506) | def handle_messages(output: Dict[str, Any], logger: Logger) -> None:
function main (line 609) | def main(config) -> None:
FILE: modules/poster_cleanarr.py
function remove_assets (line 27) | def remove_assets(
function print_output (line 119) | def print_output(
function main (line 151) | def main(config: SimpleNamespace) -> None:
FILE: modules/poster_renamerr.py
function process_file (line 35) | def process_file(file: str, new_file_path: str, action_type: str, logger...
function rename_files (line 59) | def rename_files(
function handle_output (line 216) | def handle_output(
function main (line 260) | def main(config: SimpleNamespace) -> None:
FILE: modules/renameinatorr.py
function print_output (line 15) | def print_output(output: Dict[str, Dict[str, Any]], logger: Logger) -> N...
function get_count_for_instance_type (line 62) | def get_count_for_instance_type(
function process_instance (line 90) | def process_instance(
function get_chunks_for_run (line 273) | def get_chunks_for_run(
function get_untagged_chunks_for_run (line 292) | def get_untagged_chunks_for_run(
function main (line 315) | def main(config: SimpleNamespace) -> None:
FILE: modules/sync_gdrive.py
function get_rclone_path (line 23) | def get_rclone_path() -> str:
function run_rclone (line 43) | def run_rclone(config: SimpleNamespace, logger: Logger) -> None:
function main (line 149) | def main(config: SimpleNamespace, logger: Optional[Logger] = None) -> None:
FILE: modules/unmatched_assets.py
function print_output (line 22) | def print_output(
function main (line 189) | def main(config: SimpleNamespace) -> None:
FILE: modules/upgradinatorr.py
function filter_media (line 13) | def filter_media(
function process_search_response (line 99) | def process_search_response(
function process_queue (line 133) | def process_queue(
function process_instance (line 169) | def process_instance(
function print_output (line 380) | def print_output(output_dict: Dict[str, Any], logger: Logger) -> None:
function main (line 411) | def main(config: SimpleNamespace) -> None:
FILE: util/arrpy.py
class BaseARRClient (line 17) | class BaseARRClient:
method __init__ (line 20) | def __init__(self, url: str, api: str, logger: Any) -> None:
method get_health (line 57) | def get_health(self) -> Optional[Dict[str, Any]]:
method wait_for_command (line 67) | def wait_for_command(self, command_id: int) -> bool:
method create_tag (line 95) | def create_tag(self, tag: str) -> int:
method get_instance_name (line 110) | def get_instance_name(self) -> Optional[str]:
method get_system_status (line 120) | def get_system_status(self) -> Optional[Dict[str, Any]]:
method make_get_request (line 130) | def make_get_request(
method make_post_request (line 144) | def make_post_request(
method make_put_request (line 159) | def make_put_request(
method make_delete_request (line 174) | def make_delete_request(self, endpoint: str, json: Any = None) -> Any:
method _request_with_retries (line 186) | def _request_with_retries(
method _handle_request_exception (line 226) | def _handle_request_exception(
method _get_error_hint (line 265) | def _get_error_hint(self, status_code: int) -> str:
method get_tag_id_from_name (line 285) | def get_tag_id_from_name(self, tag_name: str) -> int:
method get_all_tags (line 303) | def get_all_tags(self) -> Optional[List[Dict[str, Any]]]:
method get_quality_profile_names (line 313) | def get_quality_profile_names(self) -> Optional[Dict[str, int]]:
class RadarrClient (line 329) | class RadarrClient(BaseARRClient):
method __init__ (line 332) | def __init__(self, url: str, api: str, logger: Any) -> None:
method get_media (line 344) | def get_media(self) -> Optional[List[Dict[str, Any]]]:
method add_tags (line 354) | def add_tags(self, media_id: Union[int, List[int]], tag_id: int) -> Any:
method remove_tags (line 370) | def remove_tags(self, media_ids: List[int], tag_id: int) -> Any:
method get_rename_list (line 384) | def get_rename_list(self, media_id: int) -> Any:
method rename_media (line 395) | def rename_media(self, media_ids: List[int]) -> Any:
method rename_folders (line 411) | def rename_folders(self, media_ids: List[int], root_folder_path: str) ...
method refresh_items (line 429) | def refresh_items(self, media_ids: Union[int, List[int]]) -> Any:
method refresh_media (line 444) | def refresh_media(self) -> Any:
method search_media (line 457) | def search_media(self, media_ids: Union[int, List[int]]) -> Optional[A...
method get_movie_data (line 483) | def get_movie_data(self, media_id: int) -> Any:
method get_grab_history (line 494) | def get_grab_history(self, media_id: int) -> Any:
method get_import_history (line 506) | def get_import_history(self, media_id: int) -> Any:
method get_queue (line 518) | def get_queue(self) -> Any:
method delete_media (line 528) | def delete_media(self, media_id: int) -> Any:
method delete_movie_file (line 539) | def delete_movie_file(self, media_id: int) -> Any:
method get_parsed_media (line 550) | def get_parsed_media(self, include_episode: bool = False) -> List[Dict...
class SonarrClient (line 609) | class SonarrClient(BaseARRClient):
method __init__ (line 612) | def __init__(self, url: str, api: str, logger: Any) -> None:
method get_media (line 624) | def get_media(self) -> Optional[List[Dict[str, Any]]]:
method add_tags (line 634) | def add_tags(self, media_id: Union[int, List[int]], tag_id: int) -> Any:
method remove_tags (line 650) | def remove_tags(self, media_ids: List[int], tag_id: int) -> Any:
method get_rename_list (line 664) | def get_rename_list(self, media_id: int) -> Any:
method rename_media (line 675) | def rename_media(self, media_ids: List[int]) -> Any:
method rename_folders (line 691) | def rename_folders(self, media_ids: List[int], root_folder_path: str) ...
method refresh_items (line 709) | def refresh_items(self, media_ids: Union[int, List[int]]) -> Any:
method refresh_media (line 724) | def refresh_media(self) -> Any:
method search_media (line 737) | def search_media(self, media_ids: Union[int, List[int]]) -> Optional[A...
method search_season (line 764) | def search_season(self, media_id: int, season_number: int) -> Any:
method get_episode_data (line 781) | def get_episode_data(self, media_id: int) -> Any:
method get_episode_data_by_season (line 792) | def get_episode_data_by_season(self, media_id: int, season_number: int...
method get_season_data (line 804) | def get_season_data(self, media_id: int) -> Any:
method delete_episode_file (line 815) | def delete_episode_file(self, episode_file_id: int) -> Any:
method delete_episode_files (line 826) | def delete_episode_files(self, episode_file_ids: Union[int, List[int]]...
method search_episodes (line 841) | def search_episodes(self, episode_ids: List[int]) -> Any:
method get_grab_history (line 854) | def get_grab_history(self, media_id: int) -> Any:
method get_import_history (line 866) | def get_import_history(self, media_id: int) -> Any:
method get_season_grab_history (line 878) | def get_season_grab_history(self, media_id: int, season: int) -> Any:
method get_season_import_history (line 891) | def get_season_import_history(self, media_id: int, season: int) -> Any:
method get_queue (line 904) | def get_queue(self) -> Any:
method delete_media (line 914) | def delete_media(self, media_id: int) -> Any:
method get_parsed_media (line 925) | def get_parsed_media(self, include_episode: bool = False) -> List[Dict...
method refresh_queue (line 1021) | def refresh_queue(self) -> Any:
method remove_item_from_queue (line 1032) | def remove_item_from_queue(self, queue_ids: Union[int, List[int]]) -> ...
function create_arr_client (line 1047) | def create_arr_client(
FILE: util/assets.py
function get_assets_files (line 13) | def get_assets_files(
function merge_assets (line 67) | def merge_assets(
FILE: util/config.py
class Config (line 14) | class Config:
method __init__ (line 17) | def __init__(self, module_name: str) -> None:
method load_config (line 28) | def load_config(self) -> None:
function load_user_config (line 64) | def load_user_config(path: str) -> Dict[str, Any]:
function _reconcile_config_data (line 106) | def _reconcile_config_data(
function manage_config (line 151) | def manage_config(logger: Logger) -> None:
FILE: util/construct.py
function generate_title_variants (line 9) | def generate_title_variants(title: str) -> Dict[str, List[str]]:
function create_collection (line 47) | def create_collection(
function create_series (line 82) | def create_series(
function create_movie (line 137) | def create_movie(
FILE: util/extract.py
function extract_year (line 6) | def extract_year(text: str) -> Optional[int]:
function extract_ids (line 21) | def extract_ids(text: str) -> Tuple[Optional[int], Optional[int], Option...
FILE: util/index.py
function create_new_empty_index (line 12) | def create_new_empty_index() -> PrefixIndex:
function remove_common_words (line 21) | def remove_common_words(text: str) -> str:
function build_search_index (line 39) | def build_search_index(
function search_matches (line 92) | def search_matches(
FILE: util/logger.py
class Logger (line 14) | class Logger:
method __init__ (line 17) | def __init__(self, log_level: str, module_name: str, max_logs: int = 9):
method log_outro (line 77) | def log_outro(self) -> None:
method __getattr__ (line 89) | def __getattr__(self, name):
function _print (line 96) | def _print(*args: object, file: Optional[object] = None, **kwargs: objec...
FILE: util/match.py
function compare_strings (line 13) | def compare_strings(string1: str, string2: str) -> bool:
function is_match (line 20) | def is_match(
function match_media_to_assets (line 179) | def match_media_to_assets(
function match_assets_to_media (line 319) | def match_assets_to_media(
function handle_series_match (line 583) | def handle_series_match(
FILE: util/normalization.py
function remove_common_words (line 17) | def remove_common_words(text: str) -> str:
function remove_tokens (line 33) | def remove_tokens(text: str) -> str:
function normalize_file_names (line 47) | def normalize_file_names(file_name: str) -> str:
function normalize_titles (line 77) | def normalize_titles(title: str) -> str:
FILE: util/notification.py
class ErrorNotifyHandler (line 16) | class ErrorNotifyHandler(logging.Handler):
method __init__ (line 19) | def __init__(self, config, module_name="main", logger=None):
method emit (line 25) | def emit(self, record):
class NotifiarrConfig (line 71) | class NotifiarrConfig:
function extract_error (line 76) | def extract_error(resp: requests.Response) -> str:
function build_notifiarr_payload (line 92) | def build_notifiarr_payload(module_title: str, cid: int) -> Dict[str, Any]:
function build_discord_payload (line 121) | def build_discord_payload(
function safe_post (line 179) | def safe_post(url: str, payload: Dict[str, Any]) -> requests.Response:
function get_random_joke (line 192) | def get_random_joke() -> str:
function send_and_log_response (line 208) | def send_and_log_response(
function send_notifiarr_notification (line 233) | def send_notifiarr_notification(
function send_discord_notification (line 321) | def send_discord_notification(
function extract_apprise_errors (line 343) | def extract_apprise_errors(apprise: Apprise) -> str:
function format_notification_error (line 369) | def format_notification_error(source: Any, label: str = "") -> str:
function format_module_title (line 389) | def format_module_title(name: str) -> str:
function send_apprise_notification (line 401) | def send_apprise_notification(
function send_email_notification (line 437) | def send_email_notification(
function collect_valid_targets (line 471) | def collect_valid_targets(
function send_test_notification (line 566) | def send_test_notification(
function send_notification (line 631) | def send_notification(logger: Any, module_name: str, config: Any, output...
FILE: util/notification_formatting.py
function format_for_discord (line 7) | def format_for_discord(
function format_for_email (line 453) | def format_for_email(config: Any, output: Any) -> Tuple[str, bool]:
FILE: util/scanner.py
function scan_files_in_flat_folder (line 22) | def scan_files_in_flat_folder(folder_path: str, logger: Any) -> List[Dict]:
function scan_files_in_nested_folders (line 87) | def scan_files_in_nested_folders(folder_path: str, logger: Any) -> Optio...
function parse_folder_group (line 139) | def parse_folder_group(folder_path: str, base_name: str, files: List[str...
function parse_file_group (line 196) | def parse_file_group(folder_path: str, base_name: str, files: List[str])...
function process_files (line 267) | def process_files(folder_path: str, logger: Any) -> Optional[List[Dict]]:
function _is_asset_folders (line 304) | def _is_asset_folders(folder_path: str, logger: Any) -> bool:
function process_selected_files (line 333) | def process_selected_files(
FILE: util/scheduler.py
function check_schedule (line 13) | def check_schedule(script_name: str, schedule: str, logger: Logger) -> b...
FILE: util/utility.py
function print_json (line 22) | def print_json(data: Any, logger: Any, module_name: str, type_: str) -> ...
function print_settings (line 45) | def print_settings(logger: Any, module_config: SimpleNamespace) -> None:
function create_table (line 108) | def create_table(data: List[List[Any]]) -> str:
function get_plex_data (line 166) | def get_plex_data(
function create_bar (line 259) | def create_bar(middle_text: str) -> str:
function redact_sensitive_info (line 280) | def redact_sensitive_info(text: str, password: bool = False) -> str:
function progress (line 319) | def progress(
function redact_apis (line 365) | def redact_apis(obj: Any) -> None:
function get_log_dir (line 382) | def get_log_dir(module_name: str) -> str:
FILE: util/version.py
function get_version (line 15) | def get_version() -> str:
function _check_remote_version (line 43) | def _check_remote_version(local_version, branch, logger):
function start_version_check (line 100) | def start_version_check(config, logger, interval=3600):
FILE: web/server.py
function load_config_dict (line 43) | def load_config_dict() -> Dict[str, Any]:
function save_config_dict (line 50) | def save_config_dict(cfg: Dict[str, Any]) -> None:
class RunRequest (line 57) | class RunRequest(BaseModel):
class CancelRequest (line 63) | class CancelRequest(BaseModel):
class TestInstanceRequest (line 69) | class TestInstanceRequest(BaseModel):
class NotificationPayload (line 78) | class NotificationPayload(BaseModel):
function get_config (line 85) | def get_config() -> Dict[str, Any]:
function get_logger (line 90) | def get_logger(request: Request) -> Any:
function handle_exception (line 107) | async def handle_exception(request: FastAPIRequest, exc: Exception):
function log_route (line 116) | def log_route(logger: Any, path: str, method: str = "GET") -> None:
function get_version_route (line 123) | async def get_version_route(
function test_notification (line 136) | async def test_notification(
function root (line 162) | async def root() -> HTMLResponse:
function get_config_route (line 172) | async def get_config_route(
function update_config_route (line 181) | async def update_config_route(
function list_dir (line 227) | async def list_dir(path: str = "/") -> Any:
function get_plex_libraries (line 243) | async def get_plex_libraries(
function run_module (line 294) | async def run_module(
function module_status (line 328) | async def module_status(module: str, logger: Any = Depends(get_logger)) ...
function cancel_module (line 360) | async def cancel_module(data: CancelRequest, logger: Any = Depends(get_l...
function test_instance (line 385) | async def test_instance(
function create_folder (line 424) | async def create_folder(path: str, logger: Any = Depends(get_logger)) ->...
function serve_fragment (line 436) | async def serve_fragment(fragment_name: str, logger: Any = Depends(get_l...
function list_logs (line 449) | async def list_logs(logger: Any = Depends(get_logger)) -> Dict[str, List...
function read_log (line 473) | async def read_log(
function poster_search_stats (line 492) | async def poster_search_stats(request: Request, logger: Any = Depends(ge...
function preview_poster (line 530) | async def preview_poster(location: str, path: str, logger: Any = Depends...
function start_web_server (line 555) | def start_web_server(logger: Any) -> None:
FILE: web/static/js/common.js
constant DAPS (line 1) | const DAPS = {
function bindSaveButton (line 12) | function bindSaveButton(saveBtn, buildPayloadFn, key, postSave) {
function setSaveButtonState (line 19) | function setSaveButtonState(saveBtn, state, label = 'Save') {
function markDirty (line 40) | function markDirty() {
function saveSection (line 44) | async function saveSection(buildPayload, key, postSave, saveBtn) {
function showUnsavedModal (line 74) | function showUnsavedModal() {
function humanize (line 129) | function humanize(key) {
function showToast (line 133) | function showToast(message, type = 'info', timeout = 3000) {
FILE: web/static/js/help_content.js
constant HELP_CONTENT (line 1) | const HELP_CONTENT = {
FILE: web/static/js/helper.js
constant NOTIFICATION_LIST (line 19) | const NOTIFICATION_LIST = [
constant NOTIFICATION_DEFINITIONS (line 31) | const NOTIFICATION_DEFINITIONS = {
constant NOTIFICATION_TYPES_PER_MODULE (line 128) | const NOTIFICATION_TYPES_PER_MODULE = {
function renderHelp (line 133) | function renderHelp(sectionName) {
function renderHelpLink (line 235) | function renderHelpLink(item) {
function fetchConfig (line 244) | async function fetchConfig() {
function fetchStats (line 254) | async function fetchStats(location) {
FILE: web/static/js/index.js
function parseVersionString (line 3) | function parseVersionString(ver) {
function getRemoteBuildCount (line 28) | async function getRemoteBuildCount(owner, repo, branch) {
function mainVersionCheck (line 43) | async function mainVersionCheck() {
function setTheme (line 83) | function setTheme() {
function showSplashScreen (line 131) | function showSplashScreen() {
function animateSplashParticles (line 169) | function animateSplashParticles(canvas) {
FILE: web/static/js/instances.js
function loadInstances (line 7) | async function loadInstances() {
function createEntry (line 63) | function createEntry(service, name, settings, isNew = false) {
FILE: web/static/js/logs.js
function buildLogControls (line 10) | function buildLogControls() {
function ensureLogControls (line 45) | function ensureLogControls() {
function escapeRegex (line 78) | function escapeRegex(text) {
constant ANSI_COLORS (line 82) | const ANSI_COLORS = {
function applyLogLevelAnsiColors (line 95) | function applyLogLevelAnsiColors(line) {
function renderToXTerm (line 104) | function renderToXTerm(text, options = {}) {
function handleXTermScroll (line 135) | function handleXTermScroll() {
function loadLogs (line 146) | async function loadLogs() {
FILE: web/static/js/navigation.js
constant PAGE_LOADERS (line 10) | const PAGE_LOADERS = {
constant EDITABLE_PAGES (line 19) | const EDITABLE_PAGES = [
function isEditablePage (line 26) | function isEditablePage(currentUrl) {
function highlightNav (line 30) | function highlightNav(frag, url) {
function navigateTo (line 69) | async function navigateTo(link) {
function populateSettingsDropdown (line 124) | async function populateSettingsDropdown() {
function setupDropdownMenus (line 226) | function setupDropdownMenus() {
FILE: web/static/js/notifications.js
function loadNotifications (line 11) | async function loadNotifications() {
FILE: web/static/js/payload.js
function buildNotificationPayload (line 8) | async function buildNotificationPayload() {
function buildSchedulePayload (line 65) | async function buildSchedulePayload() {
function buildInstancesPayload (line 78) | async function buildInstancesPayload() {
function buildSettingsPayload (line 107) | async function buildSettingsPayload(moduleName) {
FILE: web/static/js/poster_search.js
constant IDS (line 4) | const IDS = {
function showLoaderModal (line 27) | function showLoaderModal(show = true) {
function formatBytes (line 60) | function formatBytes(bytes) {
function renderStatsTable (line 69) | function renderStatsTable(statsArr, totals, title) {
function sortGdriveStats (line 130) | function sortGdriveStats(statsArr, mode, priorityMap = {}) {
function fetchAndRenderStats (line 173) | async function fetchAndRenderStats() {
function renderStatsSection (line 304) | function renderStatsSection() {
function setupStatsToggle (line 352) | function setupStatsToggle() {
function getById (line 372) | function getById(id) {
function highlight (line 375) | function highlight(str, term) {
function showSpinner (line 380) | function showSpinner(show) {
function materialIcon (line 384) | function materialIcon(name, style = '') {
function showImageModal (line 388) | function showImageModal(imgSrc, caption) {
function closeImageModal (line 405) | function closeImageModal() {
function setupHoverPreview (line 411) | function setupHoverPreview() {
function fetchAllFileLists (line 427) | async function fetchAllFileLists() {
function renderResults (line 495) | function renderResults(term) {
function copyToClipboard (line 583) | function copyToClipboard(btn, text) {
function setupEventListeners (line 603) | function setupEventListeners() {
function initPosterSearch (line 707) | async function initPosterSearch() {
FILE: web/static/js/schedule.js
function loadSchedule (line 7) | async function loadSchedule() {
function isValidSchedule (line 207) | function isValidSchedule(val) {
FILE: web/static/js/settings.js
constant MODULE_RENDERERS (line 19) | const MODULE_RENDERERS = {
function loadSettings (line 34) | async function loadSettings(moduleName) {
FILE: web/static/js/settings/constants.js
constant BOOL_FIELDS (line 1) | const BOOL_FIELDS = [
constant TEXT_FIELDS (line 19) | const TEXT_FIELDS = [
constant TEXTAREA_FIELDS (line 28) | const TEXTAREA_FIELDS = [
constant INT_FIELDS (line 40) | const INT_FIELDS = [
constant JSON_FIELDS (line 49) | const JSON_FIELDS = ['token'];
constant DROP_DOWN_FIELDS (line 51) | const DROP_DOWN_FIELDS = ['log_level', 'action_type', 'app_type', 'app_i...
constant DROP_DOWN_OPTIONS (line 55) | const DROP_DOWN_OPTIONS = {
constant DIR_PICKER (line 76) | const DIR_PICKER = ['source_dirs', 'destination_dir', 'data_dir'];
constant ARR_AND_PLEX_INSTANCES (line 78) | const ARR_AND_PLEX_INSTANCES = [
constant SHOW_PLEX_IN_INSTANCE_FIELD (line 89) | const SHOW_PLEX_IN_INSTANCE_FIELD = [
constant DRAG_AND_DROP (line 95) | const DRAG_AND_DROP = {
constant LIST_FIELD (line 99) | const LIST_FIELD = {
constant PLACEHOLDER_TEXT (line 105) | const PLACEHOLDER_TEXT = {
FILE: web/static/js/settings/modal_helpers.js
function populateScheduleDropdowns (line 4) | function populateScheduleDropdowns() {
function loadHolidayPresets (line 30) | function loadHolidayPresets() {
function populateGDrivePresetsDropdown (line 77) | async function populateGDrivePresetsDropdown(gdriveSyncData, editingIdx ...
function gdrivePresets (line 190) | async function gdrivePresets() {
FILE: web/static/js/settings/modals.js
function modalFooterHtml (line 12) | function modalFooterHtml(saveId, cancelId, saveLabel = 'Save') {
function attachModalSaveCancel (line 21) | function attachModalSaveCancel(modal, saveSelector, cancelSelector, onSa...
function gdriveSyncModal (line 28) | function gdriveSyncModal(editIdx, gdriveSyncData, updateGdriveList) {
function borderReplacerrModal (line 114) | function borderReplacerrModal(editIdx, borderReplacerrData, onUpdate) {
function labelarrModal (line 255) | function labelarrModal(editIdx, labelarrData, rootConfig, updateLabelarr...
function upgradinatorrModal (line 501) | function upgradinatorrModal(editIdx, upgradinatorrData, rootConfig, upda...
function directoryPickerModal (line 642) | function directoryPickerModal(inputElement) {
FILE: web/static/js/settings/modules/border_replacerr.js
function renderReplacerrSettings (line 8) | function renderReplacerrSettings(formFields, config, rootConfig) {
function getBorderReplacerrData (line 176) | function getBorderReplacerrData() {
FILE: web/static/js/settings/modules/health_checkarr.js
function renderHealthCheckarrSettings (line 4) | function renderHealthCheckarrSettings(formFields, config, rootConfig) {
FILE: web/static/js/settings/modules/jduparr.js
function renderJduparrSettings (line 4) | function renderJduparrSettings(formFields, config) {
FILE: web/static/js/settings/modules/labelarr.js
function renderLabelarrSettings (line 8) | function renderLabelarrSettings(formFields, config, rootConfig) {
function getLabelarrData (line 152) | function getLabelarrData() {
FILE: web/static/js/settings/modules/main.js
function renderMain (line 4) | function renderMain(formFields, config) {
FILE: web/static/js/settings/modules/nohl.js
function renderNohlSettings (line 4) | function renderNohlSettings(formFields, config, rootConfig) {
FILE: web/static/js/settings/modules/poster_cleanarr.js
function renderPosterCleanarrSettings (line 4) | function renderPosterCleanarrSettings(formFields, config, rootConfig) {
FILE: web/static/js/settings/modules/poster_renamerr.js
function renderPosterRenamerrSettings (line 4) | function renderPosterRenamerrSettings(formFields, config, rootConfig) {
FILE: web/static/js/settings/modules/renameinatorr.js
function renderRenameinatorrSettings (line 4) | function renderRenameinatorrSettings(formFields, config, rootConfig) {
FILE: web/static/js/settings/modules/sync_gdrive.js
function renderGdriveSettings (line 7) | function renderGdriveSettings(formFields, config) {
function getGdriveSyncData (line 90) | function getGdriveSyncData() {
FILE: web/static/js/settings/modules/unmatched_assets.js
function renderUnmatchedAssetsSettings (line 4) | function renderUnmatchedAssetsSettings(formFields, config, rootConfig) {
FILE: web/static/js/settings/modules/upgradinatorr.js
function renderUpgradinatorrSettings (line 8) | function renderUpgradinatorrSettings(formFields, config, rootConfig) {
function getUpgradinatorrData (line 110) | function getUpgradinatorrData() {
FILE: web/static/js/settings/settings_helpers.js
function createListField (line 18) | function createListField(name, list) {
function createField (line 119) | function createField(label, html) {
function boolDropdown (line 129) | function boolDropdown(name, selected) {
function renderTextField (line 136) | function renderTextField(name, value) {
function renderBooleanField (line 163) | function renderBooleanField(name, value) {
function renderDropdownField (line 168) | function renderDropdownField(name, value, options) {
function renderTextareaArrayField (line 196) | function renderTextareaArrayField(name, values) {
function renderNumberField (line 241) | function renderNumberField(name, value) {
function renderRemoveBordersBooleanField (line 249) | function renderRemoveBordersBooleanField(config) {
function renderPlexSonarrRadarrInstancesField (line 286) | function renderPlexSonarrRadarrInstancesField(
function createDragDropField (line 499) | function createDragDropField(name, list) {
function renderField (line 607) | function renderField(formFields, key, value) {
Condensed preview — 96 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (854K chars).
[
{
"path": ".dockerignore",
"chars": 335,
"preview": "# Ignore everything\n*\n\n# Allow files and directories\n!/main.py\n!/requirements.txt\n!/jokes.txt\n!/modules\n!/util\n!/scripts"
},
{
"path": ".github/workflows/on-branch-create.yml",
"chars": 1347,
"preview": "name: Tag Docker Image for New Branch\n\non:\n create:\n branches:\n - '*' # Triggers on branch creation\n\njobs:\n d"
},
{
"path": ".github/workflows/on-branch-delete.yml",
"chars": 1477,
"preview": "name: Delete GHCR Docker Tag for Deleted Branch\n\non:\n delete:\n branches:\n - '*' # Triggers on branch deletion\n"
},
{
"path": ".github/workflows/on-commit.yml",
"chars": 2419,
"preview": "name: Update Docker Image on Branch Commit\n\non:\n push:\n branches:\n - '*' # Triggers on push to any branch\n "
},
{
"path": ".github/workflows/version.yml",
"chars": 1578,
"preview": "name: Docker Version Release\n\non:\n push:\n tags:\n - v*\n\njobs:\n\n docker-version:\n runs-on: ubuntu-latest\n \n "
},
{
"path": ".gitignore",
"chars": 720,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\ndist/\n*."
},
{
"path": "Dockerfile",
"chars": 2070,
"preview": "# Single-stage build for installing Python dependencies and required packages\nFROM python:3.11-slim \n\n# Copy requirement"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "piMIT License\n\nCopyright (c) 2023 Drazzilb\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "Makefile",
"chars": 458,
"preview": "# Create venv if it doesn't exist\n.PHONY: venv\nvenv:\n\ttest -d venv || python3 -m venv venv\n\n# Install requirements\n.PHON"
},
{
"path": "README.md",
"chars": 1628,
"preview": "\n<div align=\"center\">\n\n# DAPS\n\nAutomate, optimize, and take control of your media libraries.\n\n[',\n 'Examples: hourly(00) or"
},
{
"path": "web/static/js/helper.js",
"chars": 8643,
"preview": "import { HELP_CONTENT } from './help_content.js';\nimport { humanize } from './common.js';\n\nexport const moduleOrder = [\n"
},
{
"path": "web/static/js/index.js",
"chars": 7203,
"preview": "import { fetchConfig } from './helper.js';\n\nfunction parseVersionString(ver) {\n if (!ver) return {};\n const parts "
},
{
"path": "web/static/js/instances.js",
"chars": 7106,
"preview": "import { fetchConfig } from './helper.js';\nimport { buildInstancesPayload } from './payload.js';\n\nimport { DAPS } from '"
},
{
"path": "web/static/js/logs.js",
"chars": 14770,
"preview": "import { humanize } from './common.js';\nimport { moduleOrder } from './helper.js';\n\nlet term = null; // xterm.js instanc"
},
{
"path": "web/static/js/main.js",
"chars": 298,
"preview": "// Core system scripts\nimport './payload.js';\nimport './navigation.js';\nimport './common.js';\nimport './helper.js';\n\n// "
},
{
"path": "web/static/js/navigation.js",
"chars": 9712,
"preview": "import { loadSchedule } from './schedule.js';\nimport { loadInstances } from './instances.js';\nimport { loadLogs } from '"
},
{
"path": "web/static/js/notifications.js",
"chars": 16378,
"preview": "import {\n fetchConfig,\n NOTIFICATION_LIST,\n NOTIFICATION_DEFINITIONS,\n NOTIFICATION_TYPES_PER_MODULE,\n} from"
},
{
"path": "web/static/js/payload.js",
"chars": 10343,
"preview": "import { BOOL_FIELDS, INT_FIELDS, TEXTAREA_FIELDS, JSON_FIELDS } from './settings/constants.js';\nimport { NOTIFICATION_D"
},
{
"path": "web/static/js/poster_search.js",
"chars": 26842,
"preview": "import { fetchConfig } from './helper.js';\nimport { showToast } from './common.js';\n\nconst IDS = {\n searchInput: 'pos"
},
{
"path": "web/static/js/schedule.js",
"chars": 9455,
"preview": "import { fetchConfig, renderHelp, moduleOrder } from './helper.js';\nimport { buildSchedulePayload } from './payload.js';"
},
{
"path": "web/static/js/settings/constants.js",
"chars": 3807,
"preview": "export const BOOL_FIELDS = [\n 'dry_run',\n 'skip',\n 'sync_posters',\n 'run_border_replacerr',\n 'print_files"
},
{
"path": "web/static/js/settings/modal_helpers.js",
"chars": 8951,
"preview": "import { DROP_DOWN_OPTIONS } from './constants.js';\nimport { holidayPresets } from './presets.js';\n\nexport function popu"
},
{
"path": "web/static/js/settings/modals.js",
"chars": 35555,
"preview": "import { PLACEHOLDER_TEXT } from './constants.js';\nimport {\n populateScheduleDropdowns,\n loadHolidayPresets,\n p"
},
{
"path": "web/static/js/settings/modules/border_replacerr.js",
"chars": 7767,
"preview": "import { renderHelp } from '../../helper.js';\nimport { renderTextareaArrayField } from '../settings_helpers.js';\nimport "
},
{
"path": "web/static/js/settings/modules/health_checkarr.js",
"chars": 709,
"preview": "import { renderHelp } from '../../helper.js';\nimport { renderField, renderPlexSonarrRadarrInstancesField } from '../sett"
},
{
"path": "web/static/js/settings/modules/jduparr.js",
"chars": 481,
"preview": "import { renderField } from '../settings_helpers.js';\nimport { renderHelp } from '../../helper.js';\n\nexport function ren"
},
{
"path": "web/static/js/settings/modules/labelarr.js",
"chars": 6174,
"preview": "import { renderHelp } from '../../helper.js';\nimport { renderField } from '../settings_helpers.js';\nimport { labelarrMod"
},
{
"path": "web/static/js/settings/modules/main.js",
"chars": 467,
"preview": "import { renderField } from '../settings_helpers.js';\nimport { renderHelp } from '../../helper.js';\n\nexport function ren"
},
{
"path": "web/static/js/settings/modules/nohl.js",
"chars": 767,
"preview": "import { renderField, renderPlexSonarrRadarrInstancesField } from '../settings_helpers.js';\nimport { renderHelp } from '"
},
{
"path": "web/static/js/settings/modules/poster_cleanarr.js",
"chars": 709,
"preview": "import { renderHelp } from '../../helper.js';\nimport { renderField, renderPlexSonarrRadarrInstancesField } from '../sett"
},
{
"path": "web/static/js/settings/modules/poster_renamerr.js",
"chars": 709,
"preview": "import { renderField, renderPlexSonarrRadarrInstancesField } from '../settings_helpers.js';\nimport { renderHelp } from '"
},
{
"path": "web/static/js/settings/modules/renameinatorr.js",
"chars": 704,
"preview": "import { renderHelp } from '../../helper.js';\nimport { renderField, renderPlexSonarrRadarrInstancesField } from '../sett"
},
{
"path": "web/static/js/settings/modules/sync_gdrive.js",
"chars": 4187,
"preview": "import { gdriveSyncModal } from '../modals.js';\nimport { renderHelp } from '../../helper.js';\nimport { renderField, rend"
},
{
"path": "web/static/js/settings/modules/unmatched_assets.js",
"chars": 712,
"preview": "import { renderHelp } from '../../helper.js';\nimport { renderField, renderPlexSonarrRadarrInstancesField } from '../sett"
},
{
"path": "web/static/js/settings/modules/upgradinatorr.js",
"chars": 4022,
"preview": "import { renderField } from '../settings_helpers.js';\nimport { renderHelp } from '../../helper.js';\nimport { upgradinato"
},
{
"path": "web/static/js/settings/presets.js",
"chars": 1165,
"preview": "export const holidayPresets = {\n \"🎆 New Year's Day\": {\n schedule: 'range(12/30-01/02)',\n colors: ['#00B"
},
{
"path": "web/static/js/settings/settings_helpers.js",
"chars": 24402,
"preview": "import { directoryPickerModal } from './modals.js';\nimport {\n BOOL_FIELDS,\n TEXT_FIELDS,\n TEXTAREA_FIELDS,\n "
},
{
"path": "web/static/js/settings.js",
"chars": 3798,
"preview": "import { fetchConfig } from './helper.js';\nimport { renderPosterRenamerrSettings } from './settings/modules/poster_renam"
},
{
"path": "web/templates/index.html",
"chars": 6059,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n<script>\n(function() {\n var t;\n try { t = localStorage.getItem('theme'); "
},
{
"path": "web/templates/pages/instances.html",
"chars": 219,
"preview": "<div class=\"container-iframe\">\n <div class=\"content\">\n <form id=\"instancesForm\"></form>\n <button id=\"sa"
},
{
"path": "web/templates/pages/logs.html",
"chars": 170,
"preview": "<div class=\"container-iframe\">\n <div id=\"scroll-output-container\" style=\"position: relative\">\n <div id=\"log-ou"
},
{
"path": "web/templates/pages/notifications.html",
"chars": 389,
"preview": "<div class=\"container-iframe\">\n <div class=\"content\">\n <input\n type=\"text\"\n id=\"notifica"
},
{
"path": "web/templates/pages/poster_search.html",
"chars": 1664,
"preview": "<div class=\"container-iframe\">\n <div class=\"poster-search-loader-modal\" style=\"display: none\">\n <div class=\"te"
},
{
"path": "web/templates/pages/schedule.html",
"chars": 379,
"preview": "<div class=\"container-iframe\">\n <div class=\"content\">\n <input\n type=\"text\"\n id=\"schedule"
},
{
"path": "web/templates/pages/settings.html",
"chars": 337,
"preview": "<div class=\"container-iframe\">\n <div class=\"content\">\n <form id=\"settingsForm\">\n <div id=\"form-fiel"
}
]
About this extraction
This page contains the full source code of the Drazzilb08/daps GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 96 files (796.9 KB), approximately 181.4k tokens, and a symbol index with 346 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.