├── .gitignore ├── .github └── FUNDING.yml ├── LICENSE ├── plex-meta-manager └── config │ ├── TV Shows │ ├── Overlays.yml │ └── TV Shows.yml │ ├── Movies │ ├── Overlays.yml │ └── Movies.yml │ ├── config.yml │ └── Playlists.yml ├── prometheus └── prometheus.yml ├── .env.example ├── README.md ├── docker-compose.yml └── plex_meta_manager.py /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.buymeacoffee.com/joshdev8'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Josh Cain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plex-meta-manager/config/TV Shows/Overlays.yml: -------------------------------------------------------------------------------- 1 | templates: 2 | TV Show Status: 3 | default: 4 | weight: 50 5 | horizontal_offset: 0 6 | vertical_offset: 0 7 | conditionals: 8 | Airing: 9 | conditions: 10 | - status: airing 11 | overlay: 12 | name: Airing 13 | file: config/assets/overlays/airing.png 14 | Ended: 15 | conditions: 16 | - status: ended 17 | overlay: 18 | name: Ended 19 | file: config/assets/overlays/ended.png 20 | Canceled: 21 | conditions: 22 | - status: canceled 23 | overlay: 24 | name: Canceled 25 | file: config/assets/overlays/canceled.png 26 | 27 | overlays: 28 | IMDb Top 250: 29 | imdb_list: https://www.imdb.com/search/title/?title_type=tv_series&groups=top_250 30 | overlay: 31 | name: IMDb Top 250 32 | file: config/assets/overlays/imdb250.png 33 | horizontal_align: left 34 | vertical_align: bottom 35 | horizontal_offset: 0 36 | vertical_offset: 0 37 | 38 | Currently Airing Shows: 39 | template: {name: TV Show Status, condition: Airing} 40 | plex_all: true 41 | filters: 42 | status: airing 43 | 44 | Ended Shows: 45 | template: {name: TV Show Status, condition: Ended} 46 | plex_all: true 47 | filters: 48 | status: ended 49 | 50 | Canceled Shows: 51 | template: {name: TV Show Status, condition: Canceled} 52 | plex_all: true 53 | filters: 54 | status: canceled 55 | 56 | Streaming Badges: 57 | plex_search: 58 | any: 59 | network: "Netflix, Disney+, HBO Max, Apple TV+, Amazon, Hulu" 60 | overlay: 61 | name: Streaming Badge 62 | file: config/assets/overlays/streaming.png 63 | horizontal_align: right 64 | vertical_align: top 65 | horizontal_offset: 15 66 | vertical_offset: 15 -------------------------------------------------------------------------------- /prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Alertmanager configuration 8 | alerting: 9 | alertmanagers: 10 | - static_configs: 11 | - targets: 12 | # - alertmanager:9093 13 | 14 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 15 | rule_files: 16 | # - "first_rules.yml" 17 | # - "second_rules.yml" 18 | 19 | # A scrape configuration containing exactly one endpoint to scrape: 20 | # Here it's Prometheus itself. 21 | scrape_configs: 22 | # The job name is added as a label `job=` to any timeseries scraped from this config. 23 | - job_name: "prometheus" 24 | # metrics_path defaults to '/metrics' 25 | # scheme defaults to 'http'. 26 | static_configs: 27 | - targets: ["localhost:9090"] 28 | 29 | # Docker metrics via cAdvisor 30 | - job_name: "docker" 31 | static_configs: 32 | - targets: ["cadvisor:8080"] 33 | 34 | # Host metrics via Node Exporter 35 | - job_name: "node_exporter" 36 | static_configs: 37 | - targets: ["node-exporter:9100"] 38 | 39 | # Telegraf metrics 40 | - job_name: "telegraf" 41 | static_configs: 42 | - targets: ["telegraf:9273"] 43 | 44 | # Tautulli metrics 45 | - job_name: "tautulli" 46 | metrics_path: "/metrics" 47 | static_configs: 48 | - targets: ["tautulli:8181"] 49 | 50 | # Netdata metrics 51 | - job_name: "netdata" 52 | metrics_path: "/api/v1/allmetrics" 53 | params: 54 | format: [prometheus] 55 | honor_labels: true 56 | static_configs: 57 | - targets: ["netdata:19999"] 58 | 59 | # Plex Exporter metrics 60 | - job_name: "plex_exporter" 61 | static_configs: 62 | - targets: ["plex-exporter:9594"] -------------------------------------------------------------------------------- /plex-meta-manager/config/Movies/Overlays.yml: -------------------------------------------------------------------------------- 1 | templates: 2 | Resolution: 3 | default: 4 | weight: 50 5 | horizontal_offset: 0 6 | vertical_offset: 0 7 | conditionals: 8 | 4K: 9 | conditions: 10 | - resolution.regex: "(?i)4k|uhd|2160|4000" 11 | overlay: 12 | name: 4K 13 | file: config/assets/overlays/4K.png 14 | 1080p: 15 | conditions: 16 | - resolution.regex: "(?i)1080|1920|hd|fhd" 17 | overlay: 18 | name: 1080P 19 | file: config/assets/overlays/1080P.png 20 | 720p: 21 | conditions: 22 | - resolution.regex: "(?i)720|1280|hd" 23 | overlay: 24 | name: 720P 25 | file: config/assets/overlays/720P.png 26 | 27 | overlays: 28 | Dolby: 29 | plex_all: true 30 | filters: 31 | filepath: Dolby 32 | overlay: 33 | name: Dolby Vision 34 | file: config/assets/overlays/dolbyvision.png 35 | horizontal_align: right 36 | vertical_align: bottom 37 | horizontal_offset: 0 38 | vertical_offset: 0 39 | 40 | HDR: 41 | plex_all: true 42 | filters: 43 | filepath: HDR 44 | overlay: 45 | name: HDR 46 | file: config/assets/overlays/hdr.png 47 | horizontal_align: right 48 | vertical_align: bottom 49 | horizontal_offset: 0 50 | vertical_offset: 0 51 | 52 | IMDb Top 250: 53 | imdb_list: https://www.imdb.com/search/title/?groups=top_250&sort=user_rating,desc 54 | overlay: 55 | name: IMDb Top 250 56 | file: config/assets/overlays/imdb250.png 57 | horizontal_align: left 58 | vertical_align: bottom 59 | horizontal_offset: 0 60 | vertical_offset: 0 61 | 62 | 4K Movies: 63 | template: {name: Resolution, condition: 4K} 64 | plex_search: 65 | all: 66 | resolution.regex: "(?i)4k|uhd|2160|4000" 67 | 68 | 1080p Movies: 69 | template: {name: Resolution, condition: 1080p} 70 | plex_search: 71 | all: 72 | resolution.regex: "(?i)1080|1920|hd|fhd" 73 | 74 | 720p Movies: 75 | template: {name: Resolution, condition: 720p} 76 | plex_search: 77 | all: 78 | resolution.regex: "(?i)720|1280|hd" -------------------------------------------------------------------------------- /plex-meta-manager/config/config.yml: -------------------------------------------------------------------------------- 1 | libraries: 2 | Movies: 3 | collection_files: 4 | - file: config/Movies/Movies.yml 5 | - folder: config/Movies/ 6 | overlay_files: 7 | - remove_overlays: false 8 | - file: config/Movies/Overlays.yml 9 | TV Shows: 10 | collection_files: 11 | - file: config/TV Shows/TV Shows.yml 12 | - folder: config/TV Shows/ 13 | overlay_files: 14 | - remove_overlays: false 15 | - file: config/TV Shows/Overlays.yml 16 | playlist_files: 17 | - file: config/Playlists.yml 18 | settings: 19 | cache: true 20 | cache_expiration: 60 21 | asset_directory: config/assets 22 | asset_folders: true 23 | asset_depth: 0 24 | create_asset_folders: true 25 | dimensional_asset_rename: false 26 | download_url_assets: true 27 | show_missing_season_assets: false 28 | show_missing_episode_assets: false 29 | show_asset_not_needed: true 30 | sync_mode: append 31 | minimum_items: 1 32 | delete_below_minimum: true 33 | delete_not_scheduled: false 34 | run_again_delay: 2 35 | missing_only_released: false 36 | only_filter_missing: false 37 | show_unmanaged: true 38 | show_filtered: false 39 | show_options: false 40 | show_missing: true 41 | show_missing_assets: true 42 | save_missing: true 43 | tvdb_language: eng 44 | ignore_ids: 45 | ignore_imdb_ids: 46 | item_refresh_delay: 0 47 | verify_ssl: false 48 | playlist_sync_to_users: all 49 | prioritize_assets: false 50 | playlist_report: true 51 | show_unconfigured: true 52 | playlist_exclude_users: 53 | webhooks: 54 | error: 55 | version: 56 | run_start: 57 | run_end: 58 | changes: 59 | plex: 60 | url: ${PMM_PLEX_URL} 61 | token: ${PMM_PLEX_TOKEN} 62 | timeout: 60 63 | clean_bundles: false 64 | empty_trash: false 65 | optimize: false 66 | tmdb: 67 | apikey: ${PMM_TMDB_API_KEY} 68 | language: en 69 | cache_expiration: 60 70 | tautulli: 71 | url: ${PMM_TAUTULLI_URL} 72 | apikey: ${PMM_TAUTULLI_API_KEY} 73 | omdb: 74 | apikey: ${PMM_OMDB_API_KEY} 75 | cache_expiration: 60 76 | mdblist: 77 | apikey: ${PMM_MDBLIST_API_KEY} 78 | cache_expiration: 60 79 | trakt: 80 | client_id: ${PMM_TRAKT_CLIENT_ID} 81 | client_secret: ${PMM_TRAKT_CLIENT_SECRET} 82 | authorization: 83 | access_token: ${PMM_TRAKT_ACCESS_TOKEN} 84 | token_type: Bearer 85 | refresh_token: ${PMM_TRAKT_REFRESH_TOKEN} 86 | scope: public -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ## COPY THIS OR RENAME THIS FILE TO ".env" AND REPLACE WITH YOUR SPECIFIC VALUES 2 | ## THESE ARE ALL DUMMY VALUES BUT GIVE YOU A GENERAL IDEA OF WHAT THEY SHOULD LOOK LIKE 3 | ## for more information on configuration + env variable examples: 4 | ## https://docs.linuxserver.io/images/docker-radarr/#docker-compose-recommended-click-here-for-more-info 5 | ## (click relevant container for specific info) 6 | 7 | # Basic Configuration 8 | PATH="/usr/local/sbin:/usr/local/bin" 9 | PUID=1000 # Find more about PUID/PGID at https://docs.linuxserver.io/general/understanding-puid-and-pgid 10 | PGID=999 11 | TZ=America/New_York # See more timezone options at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 12 | USERDIR=/home/username # Path to your home directory or desired location. 13 | 14 | # Plex Specific 15 | PLEX_ADVERTISE_IP=https://192.168.0.100:32400/ # Needed in Bridge Networking. 16 | PLEX_CLAIM=aksjdfw84348033 # Obtain from https://www.plex.tv/claim. Necessary for server token. 17 | PLEX_TOKEN=aoiwejfj9230403402 # Obtain from https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ 18 | 19 | # Optional Settings 20 | EMAIL=email@email.com 21 | PASSWORD=password123 22 | HTTP_USERNAME=fakeusername 23 | HTTP_PASSWORD=fakepassword 24 | DOMAIN=mycustomdomain.com 25 | DOMAINNAME=mycustomdomain.com 26 | 27 | # Transmission Settings 28 | TRANSMISSION_USERNAME=username # Username for logging into transmission frontend 29 | TRANSMISSION_PASSWORD=password123 # Password for logging into transmission frontend 30 | 31 | # OpenVPN Configuration 32 | ## more info: https://haugene.github.io/docker-transmission-openvpn/ 33 | OPENVPN_PROVIDER=PIA # (Private Internet Access) 34 | OPENVPN_CONFIG=ca_montreal # Optional line (update this to whichever location you prefer) 35 | OPENVPN_USERNAME=openvpnusername 36 | OPENVPN_PASSWORD=openvpnpassword123 37 | 38 | # InfluxDB Setup and Operation 39 | DOCKER_INFLUXDB_INIT_MODE=setup 40 | DOCKER_INFLUXDB_INIT_USERNAME=administrator 41 | DOCKER_INFLUXDB_INIT_PASSWORD=password 42 | DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=034838492002jklasjdfasdf # not a real token 43 | DOCKER_INFLUXDB_INIT_ORG=administrator 44 | DOCKER_INFLUXDB_INIT_BUCKET=telegraf 45 | DOCKER_INFLUXDB_INIT_RETENTION=2d # Valid units: ns, us, ms, s, m, h, d, w. 46 | DOCKER_INFLUXDB_INIT_PORT=8086 47 | DOCKER_INFLUXDB_INIT_HOST=influxdb 48 | 49 | # Telegraf Configuration 50 | TELEGRAF_CFG_PATH=./telegraf/telegraf.conf 51 | 52 | # Grafana Configuration 53 | GRAFANA_PORT=3000 54 | 55 | # Radarr 56 | RADARR_URL=http://192.168.86.1:7878 57 | RADARR_API_KEY=1234523452345234jlksjdfla 58 | 59 | # Sonarr 60 | SONARR_URL=http://192.168.86.1:8989 61 | SONARR_API_KEY=28939234kajsdjkfjalsd 62 | 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoPlexx - Fully Automated Plex Media Server Setup 2 |
3 |
4 | 5 |
6 |
7 |
8 | 9 | This setup utilizes [Docker](https://www.docker.com/) and [docker-compose](https://docs.linuxserver.io/general/docker-compose) to create an automated environment for Plex Media Server with several supportive services. These instructions assume you already have both of those setup. 10 | 11 | ## Getting Started 12 | 13 | 1. Copy the `.env.example` to `.env` and update all of the values according to your setup. Notes regarding each variable are commented next the the variable name. 14 | 15 | 2. Replace the volumes in the `docker-compose.yml` file with the correct paths to your hard drive mount points. 16 | 17 | 3. **Run the Docker Compose command**: 18 | 19 | ``` 20 | docker-compose -f ~/docker/docker-compose.yml up -d 21 | ``` 22 | 23 | Replace `~/docker/docker-compose.yml` with the path to your `docker-compose.yml` file. 24 | 25 | *If you run into any issues with a specific container, copy the container name and google it for container-specific configuration FAQ's. If there is an issue with my configuration or instructions please let me know and I will update them.* 26 | 27 | Buy Me A Coffee 28 | 29 | ## Components 30 | 31 | ### Media Server 32 | 33 | - **Plex Media Server:** Central media server. 34 | - **Kometa:** Automates metadata curation of Plex content. Gives you granular control over metadata, collections, overlays, and much more. 35 | - **Cleanarr:** Finds all duplicate content on your server and intelligently selects which copy/copies to remove. 36 | - **Watchlistarr:** Connects and scans plex watchlist, sends to Radarr/Sonarr for download. 37 | 38 | ### Content Downloaders 39 | 40 | - **Radarr:** For movies. 41 | - **Sonarr:** For TV shows. 42 | - **Lidarr:** For music. 43 | - **Bazarr:** For subtitles. 44 | - **Transmission-VPN:** Torrent downloader with built-in VPN. 45 | - **Jackett:** Connects content downloaders to content sites. 46 | - **Prowlarr:** Maps content sites to Radarr + Sonarr (alternative to Jackett, easier to setup, can use both at the same time). 47 | - **Requestrr:** Enables content requests via Discord bot. 48 | 49 | ### Docker Environment Management 50 | 51 | - **Portainer:** Container management. 52 | - **Watchtower:** Automated container updates. 53 | - **Overseer:** Centralized content request and management interface. 54 | 55 | ### Monitoring 56 | 57 | - **Tautulli:** Monitors Plex Media Server usage. 58 | - **Netdata:** Live host monitoring (CPU, memory, etc.). 59 | - **Telegraf + Prometheus + InfluxDB:** Data aggregators feeding into Grafana. 60 | - **Grafana:** Visualizes metrics from Telegraf, Prometheus, and InfluxDB. 61 | -------------------------------------------------------------------------------- /plex-meta-manager/config/Playlists.yml: -------------------------------------------------------------------------------- 1 | templates: 2 | Auto Rotating Playlist: 3 | default: 4 | sync_to_users: all 5 | playlist_mode: append 6 | visible_home: true 7 | visible_shared: true 8 | delete_not_scheduled: false 9 | summary: A daily shuffled collection of movies that updates every day. 10 | schedule: daily 11 | 12 | playlists: 13 | Daily Action Movie: 14 | template: {name: Auto Rotating Playlist} 15 | plex_search: 16 | limit: 20 17 | all: 18 | genre: Action 19 | critic_rating.gte: 7.0 20 | year.gte: 1980 21 | filters: 22 | random: 20 23 | 24 | Daily Comedy Movie: 25 | template: {name: Auto Rotating Playlist} 26 | plex_search: 27 | limit: 20 28 | all: 29 | genre: Comedy 30 | audience_rating.gte: 7.5 31 | year.gte: 1980 32 | filters: 33 | random: 20 34 | 35 | Daily Animated Movie: 36 | template: {name: Auto Rotating Playlist} 37 | plex_search: 38 | limit: 15 39 | all: 40 | genre: Animation 41 | rating.gte: 7.0 42 | filters: 43 | random: 15 44 | 45 | Daily Family Movie: 46 | template: {name: Auto Rotating Playlist} 47 | plex_search: 48 | limit: 20 49 | all: 50 | genre: Family 51 | content_rating: G, PG 52 | year.gte: 1980 53 | filters: 54 | random: 15 55 | 56 | Daily TV Show Episodes: 57 | template: {name: Auto Rotating Playlist} 58 | builder_level: episode 59 | plex_search: 60 | limit: 30 61 | any: 62 | genre: Comedy, Animation 63 | year.gte: 2010 64 | filters: 65 | random: 30 66 | 67 | Top Rated Movies: 68 | template: {name: Auto Rotating Playlist} 69 | plex_search: 70 | limit: 50 71 | all: 72 | rating.gte: 8.5 73 | year.gte: 1980 74 | filters: 75 | random: 25 76 | 77 | Best Movies Past 5 Years: 78 | template: {name: Auto Rotating Playlist} 79 | plex_search: 80 | limit: 50 81 | all: 82 | year.gte: 2019 83 | year.lte: 2024 84 | rating.gte: 7.5 85 | filters: 86 | random: 20 87 | summary: A rotating selection of the best movies from the past 5 years (2019-2024) 88 | 89 | Best Movies of 2024: 90 | template: {name: Auto Rotating Playlist} 91 | plex_search: 92 | limit: 30 93 | all: 94 | year: 2024 95 | rating.gte: 7.0 96 | filters: 97 | random: 15 98 | summary: A rotating selection of the best movies released in 2024 99 | 100 | Anticipated Movies of 2025: 101 | template: {name: Auto Rotating Playlist} 102 | tmdb_discover: 103 | primary_release_year: 2025 104 | sort_by: popularity.desc 105 | limit: 30 106 | filters: 107 | random: 15 108 | summary: A rotating selection of upcoming and anticipated movies for 2025 109 | 110 | Movie of the Day: 111 | template: {name: Auto Rotating Playlist} 112 | plex_search: 113 | limit: 1 114 | all: 115 | rating.gte: 8.0 116 | resolution: 1080p, 4k 117 | filters: 118 | random: 1 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | networks: 4 | media_network: 5 | driver: bridge 6 | monitoring_network: 7 | driver: bridge 8 | download_network: 9 | driver: bridge 10 | 11 | x-common-vars: &common-vars 12 | PUID: ${PUID} 13 | PGID: ${PGID} 14 | TZ: ${TZ} 15 | 16 | x-common-volumes: &common-volumes >- 17 | ${USERDIR}/docker/certs:/certs:ro, 18 | /etc/localtime:/etc/localtime:ro 19 | 20 | services: 21 | portainer: 22 | image: portainer/portainer-ce:latest 23 | container_name: portainer 24 | restart: unless-stopped 25 | networks: 26 | - monitoring_network 27 | volumes: 28 | - /var/run/docker.sock:/var/run/docker.sock 29 | - portainer-data:/data 30 | - *common-volumes 31 | ports: 32 | - "9000:9000" 33 | environment: 34 | <<: *common-vars 35 | 36 | plexms: 37 | container_name: plexms 38 | restart: unless-stopped 39 | image: plexinc/pms-docker:plexpass 40 | volumes: 41 | - ${USERDIR}/docker/plexms:/config 42 | - ${USERDIR}/Downloads/plex_tmp:/transcode 43 | - /media/media:/media 44 | - *common-volumes 45 | network_mode: host 46 | environment: 47 | <<: *common-vars 48 | HOSTNAME: ${PLEX_HOSTNAME} 49 | PLEX_CLAIM: ${PLEX_CLAIM} 50 | ADVERTISE_IP: ${PLEX_ADVERTISE_IP} 51 | 52 | tautulli: 53 | image: ghcr.io/linuxserver/tautulli:latest 54 | container_name: tautulli 55 | restart: unless-stopped 56 | networks: 57 | - media_network 58 | volumes: 59 | - ${USERDIR}/docker/plexpy:/config 60 | - /var/log/plexpy:/logs:ro 61 | - *common-volumes 62 | ports: 63 | - "8181:8181" 64 | environment: 65 | <<: *common-vars 66 | depends_on: 67 | - plexms 68 | 69 | watchtower: 70 | container_name: watchtower 71 | restart: unless-stopped 72 | image: nickfedor/watchtower:latest 73 | volumes: 74 | - /var/run/docker.sock:/var/run/docker.sock 75 | command: --schedule "0 0 4 * * *" --cleanup 76 | 77 | transmission-vpn: 78 | container_name: transmission-vpn 79 | image: haugene/transmission-openvpn 80 | networks: 81 | - download_network 82 | cap_add: 83 | - NET_ADMIN 84 | devices: 85 | - /dev/net/tun 86 | restart: unless-stopped 87 | ports: 88 | - "9091:9091" 89 | dns: 90 | - 8.8.8.8 91 | - 8.8.4.4 92 | volumes: 93 | - ${USERDIR}/docker/transmission-vpn:/data 94 | - ${USERDIR}/Downloads:/data/watch 95 | - ${USERDIR}/Downloads/completed:/data/completed 96 | - ${USERDIR}/Downloads/incomplete:/data/incomplete 97 | - *common-volumes 98 | environment: 99 | <<: *common-vars 100 | OPENVPN_PROVIDER: ${OPENVPN_PROVIDER} 101 | OPENVPN_USERNAME: ${OPENVPN_USERNAME} 102 | OPENVPN_PASSWORD: ${OPENVPN_PASSWORD} 103 | OPENVPN_CONFIG: ${OPENVPN_CONFIG} 104 | OPENVPN_OPTS: "--inactive 3600 --ping 10 --ping-exit 60 --mute-replay-warnings" 105 | LOCAL_NETWORK: "192.168.86.0/24" 106 | TRANSMISSION_RPC_AUTHENTICATION_REQUIRED: "true" 107 | TRANSMISSION_RPC_HOST_WHITELIST: "127.0.0.*,192.168.*.*" 108 | TRANSMISSION_PASSWORD: ${TRANSMISSION_PASSWORD} 109 | TRANSMISSION_USERNAME: ${TRANSMISSION_USERNAME} 110 | TRANSMISSION_UMASK: "2" 111 | TRANSMISSION_RATIO_LIMIT_ENABLED: "true" 112 | TRANSMISSION_RATIO_LIMIT: "0" 113 | 114 | jackett: 115 | image: linuxserver/jackett:latest 116 | container_name: jackett 117 | networks: 118 | - download_network 119 | volumes: 120 | - ${USERDIR}/docker/jackett:/config 121 | - *common-volumes 122 | ports: 123 | - "9117:9117" 124 | restart: unless-stopped 125 | environment: 126 | <<: *common-vars 127 | 128 | sonarr: 129 | image: mdhiggins/sonarr-sma:latest 130 | container_name: sonarr 131 | networks: 132 | - media_network 133 | - download_network 134 | volumes: 135 | - /opt/appdata/sma:/usr/local/sma/config 136 | - ${USERDIR}/docker/sonarr:/config 137 | - ${USERDIR}/Downloads/completed:/data/completed 138 | - ${USERDIR}/Downloads/completed/recyclingBin:/data/completed/recyclingBin 139 | - *common-volumes 140 | ports: 141 | - "8989:8989" 142 | restart: unless-stopped 143 | environment: 144 | <<: *common-vars 145 | 146 | radarr: 147 | image: mdhiggins/radarr-sma:latest 148 | container_name: radarr 149 | networks: 150 | - media_network 151 | - download_network 152 | volumes: 153 | - /opt/appdata/sma:/usr/local/sma/config 154 | - ${USERDIR}/docker/radarr:/config 155 | - ${USERDIR}/media/movies:/movies 156 | - ${USERDIR}/Downloads/completed:/data/completed 157 | - ${USERDIR}/Downloads/completed/recyclingBin:/data/completed/recyclingBin 158 | - *common-volumes 159 | ports: 160 | - "7878:7878" 161 | restart: unless-stopped 162 | environment: 163 | <<: *common-vars 164 | 165 | influxdb: 166 | image: influxdb:latest 167 | networks: 168 | - monitoring_network 169 | volumes: 170 | - influxdb-data:/var/lib/influxdb2 171 | env_file: 172 | - .env 173 | restart: unless-stopped 174 | ports: 175 | - "${DOCKER_INFLUXDB_INIT_PORT}:8086" 176 | 177 | telegraf: 178 | image: telegraf:latest 179 | networks: 180 | - monitoring_network 181 | volumes: 182 | - ${TELEGRAF_CFG_PATH}:/etc/telegraf/telegraf.conf:ro 183 | restart: unless-stopped 184 | env_file: 185 | - .env 186 | depends_on: 187 | - influxdb 188 | 189 | grafana: 190 | image: grafana/grafana-oss:latest 191 | networks: 192 | - monitoring_network 193 | volumes: 194 | - grafana-data:/var/lib/grafana 195 | depends_on: 196 | - influxdb 197 | restart: unless-stopped 198 | ports: 199 | - "${GRAFANA_PORT}:3000" 200 | environment: 201 | <<: *common-vars 202 | 203 | bazarr: 204 | image: lscr.io/linuxserver/bazarr:latest 205 | container_name: bazarr 206 | networks: 207 | - media_network 208 | volumes: 209 | - ${USERDIR}/docker/bazarr/config:/config 210 | - *common-volumes 211 | ports: 212 | - "6767:6767" 213 | restart: unless-stopped 214 | environment: 215 | <<: *common-vars 216 | depends_on: 217 | - radarr 218 | - sonarr 219 | 220 | netdata: 221 | image: netdata/netdata:latest 222 | container_name: netdata2 223 | networks: 224 | - monitoring_network 225 | ports: 226 | - "19999:19999" 227 | restart: unless-stopped 228 | cap_add: 229 | - SYS_PTRACE 230 | security_opt: 231 | - apparmor:unconfined 232 | volumes: 233 | - netdataconfig:/etc/netdata 234 | - netdatalib:/var/lib/netdata 235 | - netdatacache:/var/cache/netdata 236 | - /etc/passwd:/host/etc/passwd:ro 237 | - /etc/group:/host/etc/group:ro 238 | - /proc:/host/proc:ro 239 | - /sys:/host/sys:ro 240 | - /etc/os-release:/host/etc/os-release:ro 241 | 242 | overseerr: 243 | image: lscr.io/linuxserver/overseerr:develop 244 | container_name: overseerr 245 | networks: 246 | - media_network 247 | volumes: 248 | - ${USERDIR}/docker/overseer/config:/config 249 | ports: 250 | - "5055:5055" 251 | restart: unless-stopped 252 | environment: 253 | <<: *common-vars 254 | depends_on: 255 | - plexms 256 | 257 | plex-meta-manager: 258 | image: kometateam/kometa:latest 259 | container_name: kometa 260 | networks: 261 | - media_network 262 | environment: 263 | <<: *common-vars 264 | PMM_CONFIG: /config/config.yml 265 | PMM_TIME: "03:00" 266 | PMM_RUN: "True" 267 | PMM_TEST: "False" 268 | PMM_NO_MISSING: "False" 269 | PMM_PLEX_URL: ${PLEX_ADVERTISE_IP} 270 | PMM_PLEX_TOKEN: ${PLEX_TOKEN} 271 | PMM_TMDB_API_KEY: ${TMDB_API_KEY} 272 | PMM_TAUTULLI_URL: http://tautulli:8181 273 | PMM_TAUTULLI_API_KEY: ${TAUTULLI_API_KEY} 274 | volumes: 275 | - ${USERDIR}/docker/plex-meta-manager/config:/config 276 | restart: unless-stopped 277 | depends_on: 278 | - plexms 279 | 280 | prometheus: 281 | image: prom/prometheus:latest 282 | networks: 283 | - monitoring_network 284 | ports: 285 | - "9090:9090" 286 | restart: unless-stopped 287 | volumes: 288 | - prometheus-data:/prometheus 289 | - ${USERDIR}/docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro 290 | command: 291 | - "--config.file=/etc/prometheus/prometheus.yml" 292 | - "--storage.tsdb.path=/prometheus" 293 | - "--web.enable-lifecycle" 294 | 295 | prowlarr: 296 | image: lscr.io/linuxserver/prowlarr:latest 297 | container_name: prowlarr 298 | networks: 299 | - download_network 300 | volumes: 301 | - ${USERDIR}/docker/prowlarr:/config 302 | ports: 303 | - "9696:9696" 304 | restart: unless-stopped 305 | environment: 306 | <<: *common-vars 307 | 308 | requestrr: 309 | image: darkalfx/requestrr:latest 310 | container_name: requestrr 311 | networks: 312 | - media_network 313 | ports: 314 | - "4545:4545" 315 | environment: 316 | <<: *common-vars 317 | volumes: 318 | - ${USERDIR}/docker/requestrr/config:/root/config 319 | restart: unless-stopped 320 | depends_on: 321 | - radarr 322 | - sonarr 323 | 324 | cleanarr: 325 | image: selexin/cleanarr:latest 326 | container_name: cleanarr 327 | networks: 328 | - media_network 329 | ports: 330 | - "5000:80" 331 | environment: 332 | <<: *common-vars 333 | PLEX_TOKEN: ${PLEX_TOKEN} 334 | PLEX_BASE_URL: ${PLEX_ADVERTISE_IP} 335 | LIBRARY_NAMES: "Movies;TV Shows" 336 | volumes: 337 | - ${DOCKER_DIR}/cleanarr/config:/config 338 | restart: unless-stopped 339 | depends_on: 340 | - plexms 341 | 342 | watchlistarr: 343 | image: nylonee/watchlistarr 344 | environment: 345 | SONARR_API_KEY: ${SONARR_API_KEY} 346 | SONARR_BASE_URL: ${SONARR_URL} 347 | RADARR_API_KEY: ${RADARR_API_KEY} 348 | RADARR_BASE_URL: ${RADARR_URL} 349 | PLEX_TOKEN: ${PLEX_TOKEN} 350 | PUID: ${PUID} 351 | PGID: ${PGID} 352 | TZ: ${TZ} 353 | volumes: 354 | - ${USERDIR}/docker/watchlistarr/config:/app/config 355 | restart: unless-stopped 356 | 357 | volumes: 358 | netdataconfig: 359 | netdatalib: 360 | netdatacache: 361 | grafana-data: 362 | influxdb-data: 363 | prometheus-data: 364 | portainer-data: 365 | -------------------------------------------------------------------------------- /plex-meta-manager/config/TV Shows/TV Shows.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | ############################ 3 | # POPULAR # 4 | ############################ 5 | 6 | IMDb Most Popular: 7 | imdb_chart: popular_shows 8 | sync_mode: sync 9 | collection_mode: default 10 | collection_order: custom 11 | sort_title: "*100" 12 | schedule: daily 13 | TMDb Most Popular: 14 | tmdb_popular: 100 15 | sync_mode: sync 16 | collection_mode: default 17 | collection_order: custom 18 | sort_title: "*101" 19 | schedule: daily 20 | Trakt Trending: 21 | trakt_trending: 30 22 | sync_mode: sync 23 | collection_mode: default 24 | collection_order: custom 25 | sort_title: "*102" 26 | schedule: daily 27 | 28 | ############################ 29 | # DISCOVER # 30 | ############################ 31 | 32 | Top Rated Animated Shows: 33 | tmdb_discover: 34 | limit: 30 35 | include_adult: false 36 | with_genres: 16 37 | with_original_language: en 38 | sort_by: vote_average.desc 39 | vote_count.gte: 100 40 | first_air_date.gte: 2010-01-01 41 | sync_mode: sync 42 | collection_mode: default 43 | collection_order: custom 44 | summary: A collection of the highest rated animated TV shows 45 | sort_title: "*150" 46 | schedule: weekly 47 | 48 | Top Rated Family Shows: 49 | tmdb_discover: 50 | limit: 30 51 | include_adult: false 52 | with_genres: 10751 53 | sort_by: vote_average.desc 54 | vote_count.gte: 50 55 | sync_mode: sync 56 | collection_mode: default 57 | collection_order: custom 58 | summary: A collection of the highest rated family-friendly TV shows 59 | sort_title: "*151" 60 | schedule: weekly 61 | 62 | ############################ 63 | # CUSTOM # 64 | ############################ 65 | 66 | Star Trek: 67 | tmdb_list: https://www.themoviedb.org/list/7071553 68 | sync_mode: sync 69 | collection_mode: default 70 | collection_order: release 71 | sort_title: "*400" 72 | file_poster: config/assets/Star Trek-2.png 73 | schedule: monthly(10) 74 | Star Wars (Animated): 75 | tmdb_list: https://www.themoviedb.org/list/7091278 76 | sync_mode: sync 77 | collection_mode: default 78 | collection_order: release 79 | sort_title: "*401" 80 | schedule: monthly(10) 81 | Star Wars (Live Action): 82 | tmdb_list: https://www.themoviedb.org/list/8173369 83 | sync_mode: sync 84 | collection_mode: default 85 | collection_order: release 86 | sort_title: "*402" 87 | schedule: monthly(10) 88 | Classic Cartoons: 89 | tmdb_list: https://www.themoviedb.org/list/7088117 90 | sync_mode: sync 91 | collection_mode: default 92 | collection_order: alpha 93 | sort_title: "*403" 94 | schedule: yearly(01/30) 95 | Nature Documentary: 96 | tmdb_list: https://www.themoviedb.org/list/7087971 97 | sync_mode: sync 98 | collection_mode: default 99 | collection_order: release 100 | sort_title: "*404" 101 | schedule: monthly(10) 102 | Prison: 103 | tmdb_list: https://www.themoviedb.org/list/7104835 104 | sync_mode: sync 105 | collection_mode: default 106 | collection_order: alpha 107 | sort_title: "*405" 108 | schedule: monthly(10) 109 | Arrowverse: 110 | tmdb_list: https://www.themoviedb.org/list/7109881 111 | sync_mode: sync 112 | collection_mode: default 113 | collection_order: alpha 114 | sort_title: "*406" 115 | schedule: monthly(10) 116 | Baking: 117 | tmdb_list: https://www.themoviedb.org/list/8168660 118 | tvdb_show: 119 | - 407030 120 | sync_mode: sync 121 | collection_mode: default 122 | collection_order: release 123 | sort_title: "*407" 124 | schedule: monthly(10) 125 | My Favorite TV Shows: 126 | tmdb_list: https://www.themoviedb.org/list/7111217 127 | sync_mode: sync 128 | collection_mode: default 129 | collection_order: alpha 130 | sort_title: "*450" 131 | schedule: monthly(10) 132 | 133 | ############################ 134 | # STUDIOS # 135 | ############################ 136 | 137 | Marvel: 138 | tmdb_list: https://www.themoviedb.org/list/7071556 139 | sync_mode: sync 140 | collection_mode: default 141 | collection_order: release 142 | sort_title: "*500" 143 | schedule: weekly(friday) 144 | Marvel Studios: 145 | tmdb_company: 420 146 | sync_mode: sync 147 | collection_mode: default 148 | collection_order: release 149 | sort_title: "*501" 150 | schedule: weekly(friday) 151 | DC: 152 | tmdb_list: https://www.themoviedb.org/list/7071946 153 | sync_mode: sync 154 | collection_mode: default 155 | collection_order: release 156 | sort_title: "*502" 157 | schedule: weekly(friday) 158 | 159 | ############################ 160 | # NETWORKS # 161 | ############################ 162 | 163 | Apple TV+: 164 | tmdb_network: 2552 165 | sync_mode: sync 166 | collection_mode: default 167 | collection_order: release 168 | sort_title: "*700" 169 | schedule: daily 170 | Apple TV+ (Score): 171 | mdblist_list: https://mdblist.com/lists/awesomeaustn/apple-tv-sorted-by-score 172 | sync_mode: sync 173 | collection_mode: default 174 | collection_order: custom 175 | sort_title: "*700b" 176 | summary: Sorted by a combined score of IMDb, Trakt, TMDb, Letterboxed, Rotten Tomatoes, Audience, Metacritic, and RogerEbert. 177 | schedule: daily 178 | Netflix: 179 | tmdb_network: 213 180 | sync_mode: sync 181 | collection_mode: default 182 | collection_order: release 183 | sort_title: "*701" 184 | schedule: daily 185 | Netflix (Score): 186 | mdblist_list: https://mdblist.com/lists/awesomeaustn/netflix-sorted-by-score 187 | sync_mode: sync 188 | collection_mode: default 189 | collection_order: custom 190 | sort_title: "*701b" 191 | summary: Sorted by a combined score of IMDb, Trakt, TMDb, Letterboxed, Rotten Tomatoes, Audience, Metacritic, and RogerEbert. 192 | schedule: daily 193 | Prime Video: 194 | tmdb_network: 1024 195 | sync_mode: sync 196 | collection_mode: default 197 | collection_order: release 198 | sort_title: "*702" 199 | schedule: daily 200 | Hulu: 201 | tmdb_network: 453 202 | sync_mode: sync 203 | collection_mode: default 204 | collection_order: release 205 | sort_title: "*703" 206 | schedule: daily 207 | Max: 208 | tmdb_network: 3186 209 | sync_mode: sync 210 | collection_mode: default 211 | collection_order: release 212 | sort_title: "*704" 213 | summary: Formerly HBO Max 214 | schedule: daily 215 | Disney+: 216 | tmdb_network: 2739 217 | sync_mode: sync 218 | collection_mode: default 219 | collection_order: release 220 | sort_title: "*705" 221 | schedule: daily 222 | Disney+ (Score): 223 | mdblist_list: https://mdblist.com/lists/awesomeaustn/disney-sorted-by-score 224 | sync_mode: sync 225 | collection_mode: default 226 | collection_order: custom 227 | sort_title: "*705b" 228 | summary: Sorted by a combined score of IMDb, Trakt, TMDb, Letterboxed, Rotten Tomatoes, Audience, Metacritic, and RogerEbert. 229 | schedule: daily 230 | Paramount+: 231 | tmdb_network: 4330 232 | sync_mode: sync 233 | collection_mode: default 234 | collection_order: release 235 | sort_title: "*706" 236 | schedule: daily 237 | 238 | ############################ 239 | # GENRES # 240 | ############################ 241 | 242 | Comedy: 243 | plex_search: 244 | all: 245 | genre: Comedy 246 | sync_mode: sync 247 | collection_mode: default 248 | collection_order: alpha 249 | sort_title: "*200" 250 | summary: A collection of Comedy TV Shows 251 | schedule: weekly(friday) 252 | Good Comedy: 253 | plex_search: 254 | all: 255 | genre: Comedy 256 | rating.gte: 8 257 | sync_mode: sync 258 | collection_mode: default 259 | collection_order: alpha 260 | sort_title: "*201" 261 | summary: A collection of Comedy TV Shows with a rating of 8 and higher 262 | schedule: weekly(friday) 263 | Drama: 264 | plex_search: 265 | all: 266 | genre: Drama 267 | sync_mode: sync 268 | collection_mode: default 269 | collection_order: alpha 270 | sort_title: "*202" 271 | summary: A collection of Drama TV Shows 272 | schedule: weekly(friday) 273 | Good Drama: 274 | plex_search: 275 | all: 276 | genre: Drama 277 | rating.gte: 8 278 | sync_mode: sync 279 | collection_mode: default 280 | collection_order: alpha 281 | sort_title: "*203" 282 | summary: A collection of Drama TV Shows with a rating of 8 and higher 283 | schedule: weekly(friday) 284 | Animation: 285 | plex_search: 286 | all: 287 | genre: Animation 288 | sync_mode: sync 289 | collection_mode: default 290 | collection_order: alpha 291 | sort_title: "*204" 292 | summary: A collection of Animated TV Shows 293 | schedule: weekly(friday) 294 | Sci-Fi & Fantasy: 295 | plex_search: 296 | all: 297 | genre: Science Fiction 298 | sync_mode: sync 299 | collection_mode: default 300 | collection_order: alpha 301 | sort_title: "*205" 302 | summary: A collection of Science Fiction TV Shows 303 | schedule: weekly(friday) 304 | 305 | ############################ 306 | # YEARS # 307 | ############################ 308 | 309 | 2020 Shows: 310 | plex_search: 311 | all: 312 | year: 2020 313 | sync_mode: sync 314 | collection_mode: default 315 | collection_order: release 316 | summary: A collection of TV shows in 2020 317 | sort_title: "*300" 318 | schedule: weekly(friday) 319 | 2021 Shows: 320 | plex_search: 321 | all: 322 | year: 2021 323 | sync_mode: sync 324 | collection_mode: default 325 | collection_order: release 326 | summary: A collection of TV shows in 2021 327 | sort_title: "*301" 328 | schedule: weekly(friday) 329 | 2022 Shows: 330 | plex_search: 331 | all: 332 | year: 2022 333 | sync_mode: sync 334 | collection_mode: default 335 | collection_order: release 336 | summary: A collection of TV shows in 2022 337 | sort_title: "*302" 338 | schedule: weekly(friday) 339 | 2023 Shows: 340 | plex_search: 341 | all: 342 | year: 2023 343 | sync_mode: sync 344 | collection_mode: default 345 | collection_order: release 346 | summary: A collection of TV shows in 2023 347 | sort_title: "*303" 348 | schedule: weekly(friday) 349 | 2024 Shows: 350 | plex_search: 351 | all: 352 | year: 2024 353 | sync_mode: sync 354 | collection_mode: default 355 | collection_order: release 356 | summary: A collection of TV shows premiered in 2024 357 | sort_title: "*304" 358 | schedule: daily 359 | 2025 Shows: 360 | plex_search: 361 | all: 362 | year: 2025 363 | sync_mode: sync 364 | collection_mode: default 365 | collection_order: release 366 | summary: A collection of TV shows premiering in 2025 367 | sort_title: "*305" 368 | schedule: daily 369 | 370 | ############################ 371 | # STATUS # 372 | ############################ 373 | 374 | Currently Airing: 375 | plex_all: true 376 | filters: 377 | status: airing 378 | sync_mode: sync 379 | collection_mode: default 380 | collection_order: alpha 381 | sort_title: "*600" 382 | schedule: weekly 383 | 384 | Recently Ended: 385 | plex_all: true 386 | filters: 387 | status: ended 388 | year.gte: 2022 389 | sync_mode: sync 390 | collection_mode: default 391 | collection_order: alpha 392 | sort_title: "*601" 393 | schedule: weekly 394 | 395 | ############################ 396 | # AUDIO # 397 | ############################ 398 | 399 | ############################ 400 | # ACTORS # 401 | ############################ 402 | 403 | ############################ 404 | # DIRECTORS # 405 | ############################ 406 | 407 | ############################ 408 | # DOLBY # 409 | ############################ 410 | 411 | # Dolby Atmos & Dolby Vision: 412 | # mdblist_list: https://mdblist.com/lists/blisskodi/dolby-atmos-dolby-vision-tv 413 | # sync_mode: sync 414 | # collection_mode: default 415 | # collection_order: custom 416 | # sort_title: "*952" 417 | # schedule: weekly(friday) 418 | # The default sort_by when it's not specified is score.desc 419 | -------------------------------------------------------------------------------- /plex-meta-manager/config/Movies/Movies.yml: -------------------------------------------------------------------------------- 1 | templates: 2 | ##################################### 3 | # HOLIDAY TEMPLATE BY DRAZZIZZI # 4 | ##################################### 5 | 6 | Holiday: 7 | smart_label: title.asc 8 | summary: A timed collection of <> movies and other movies that may relate to the holiday. This collection will automatically disappear once the holiday period is over. 9 | delete_not_scheduled: true 10 | sort_title: "!AA" 11 | visible_home: true 12 | visible_shared: true 13 | sync_mode: sync 14 | 15 | collections: 16 | ############################ 17 | # POPULAR # 18 | ############################ 19 | 20 | IMDb Top 250: 21 | imdb_list: https://www.imdb.com/search/title/?groups=top_250&sort=user_rating,desc 22 | sync_mode: sync 23 | collection_mode: default 24 | collection_order: custom 25 | sort_title: "*100" 26 | schedule: daily 27 | TMDb Top Rated: 28 | tmdb_top_rated: 30 29 | sync_mode: sync 30 | collection_mode: default 31 | collection_order: custom 32 | sort_title: "*101" 33 | schedule: daily 34 | TMDb Most Popular: 35 | tmdb_popular: 30 36 | sync_mode: sync 37 | collection_mode: default 38 | collection_order: custom 39 | sort_title: "*102" 40 | schedule: daily 41 | TMDb Weekly Trending: 42 | tmdb_trending_weekly: 30 43 | sync_mode: sync 44 | collection_mode: default 45 | collection_order: custom 46 | sort_title: "*103" 47 | schedule: daily 48 | Trakt Trending: 49 | trakt_trending: 30 50 | sync_mode: sync 51 | collection_mode: default 52 | collection_order: custom 53 | sort_title: "*104" 54 | schedule: daily 55 | 56 | ############################ 57 | # DISCOVER # 58 | ############################ 59 | 60 | Animated Kids Most Popular: 61 | tmdb_discover: 62 | limit: 30 63 | with_genres: 16 64 | certification_country: US 65 | certification: G|PG 66 | sort_by: popularity.desc 67 | sync_mode: sync 68 | collection_mode: default 69 | collection_order: custom 70 | summary: A collection of the most popular Animated Kids movies from TMDb 71 | sort_title: "*150" 72 | schedule: daily 73 | 74 | ############################ 75 | # TRAKT COLLECTIONS # 76 | ############################ 77 | 78 | Trending Now (Unplayed): 79 | trakt_chart: 80 | chart: trending 81 | limit: 100 82 | sort_title: "!B Trending Now" 83 | sync_mode: sync 84 | collection_mode: default 85 | collection_filtering: user 86 | visible_library: true 87 | visible_home: true 88 | visible_shared: true 89 | smart_label: 90 | sort_by: release.desc 91 | all: 92 | unplayed: true 93 | schedule: daily 94 | Most Watched This Week (Unplayed): 95 | trakt_chart: 96 | chart: watched 97 | time_period: weekly 98 | limit: 100 99 | sort_title: "!B Watched This Week" 100 | sync_mode: sync 101 | collection_mode: default 102 | collection_filtering: user 103 | visible_library: false 104 | visible_home: false 105 | visible_shared: false 106 | smart_label: 107 | sort_by: release.desc 108 | all: 109 | unplayed: true 110 | schedule: daily 111 | Most Watched This Year (Unplayed): 112 | trakt_chart: 113 | chart: watched 114 | time_period: yearly 115 | limit: 200 116 | sort_title: "!B Watched This Year" 117 | sync_mode: sync 118 | collection_mode: default 119 | collection_filtering: user 120 | visible_library: false 121 | visible_home: false 122 | visible_shared: false 123 | smart_label: 124 | sort_by: release.desc 125 | all: 126 | unplayed: true 127 | schedule: daily 128 | 129 | ############################ 130 | # BEST OF # 131 | ############################ 132 | 133 | Best Picture: 134 | imdb_list: https://www.imdb.com/list/ls560768259 135 | sync_mode: sync 136 | collection_mode: default 137 | collection_order: custom 138 | sort_title: "*105" 139 | schedule: yearly(03/01) 140 | Best Animated Feature Film: 141 | imdb_list: https://www.imdb.com/list/ls560364561 142 | sync_mode: sync 143 | collection_mode: default 144 | collection_order: custom 145 | sort_title: "*105b" 146 | schedule: yearly(03/01) 147 | Best Cinematography: 148 | imdb_list: https://www.imdb.com/list/ls560369098 149 | sync_mode: sync 150 | collection_mode: default 151 | collection_order: custom 152 | sort_title: "*105c" 153 | schedule: yearly(03/01) 154 | Best Film Editing: 155 | imdb_list: https://www.imdb.com/list/ls560364494 156 | sync_mode: sync 157 | collection_mode: default 158 | collection_order: custom 159 | sort_title: "*105d" 160 | schedule: yearly(03/01) 161 | Best Sound: 162 | imdb_list: https://www.imdb.com/list/ls560369669 163 | sync_mode: sync 164 | collection_mode: default 165 | collection_order: custom 166 | sort_title: "*105e" 167 | schedule: yearly(03/01) 168 | Best Visual Effects: 169 | imdb_list: https://www.imdb.com/list/ls560368042 170 | sync_mode: sync 171 | collection_mode: default 172 | collection_order: custom 173 | sort_title: "*105f" 174 | schedule: yearly(03/01) 175 | "AFI's 100": 176 | imdb_list: https://www.imdb.com/list/ls027841309 177 | sync_mode: sync 178 | collection_mode: default 179 | collection_order: custom 180 | sort_title: "*106" 181 | summary: AFI's 100 Years... 100 Movies – 10th Anniversary Edition (2007) is an updated edition to AFI"s 100 Years... 100 Movies, a list of the top 100 greatest American films of all time. Honoring the 10th anniversary of this award-winning series, a jury of 1,500 film artists, critics, and historians determined that Citizen Kane remained the greatest movie of all time. AFI stands for American Film Institute. 182 | schedule: yearly(01/30) 183 | Rotten Tomatoes - Best of 2014: 184 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2014 185 | sync_mode: sync 186 | collection_mode: default 187 | collection_order: custom 188 | sort_title: "*107" 189 | schedule: yearly(01/30) 190 | Rotten Tomatoes - Best of 2015: 191 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2015 192 | sync_mode: sync 193 | collection_mode: default 194 | collection_order: custom 195 | sort_title: "*108" 196 | schedule: yearly(01/30) 197 | Rotten Tomatoes - Best of 2016: 198 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2016 199 | sync_mode: sync 200 | collection_mode: default 201 | collection_order: custom 202 | sort_title: "*109" 203 | schedule: yearly(01/30) 204 | Rotten Tomatoes - Best of 2017: 205 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2017 206 | sync_mode: sync 207 | collection_mode: default 208 | collection_order: custom 209 | sort_title: "*110" 210 | schedule: yearly(01/30) 211 | Rotten Tomatoes - Best of 2018: 212 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2018 213 | sync_mode: sync 214 | collection_mode: default 215 | collection_order: custom 216 | sort_title: "*111" 217 | schedule: yearly(01/30) 218 | Rotten Tomatoes - Best of 2019: 219 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2019 220 | sync_mode: sync 221 | collection_mode: default 222 | collection_order: custom 223 | sort_title: "*112" 224 | schedule: yearly(01/30) 225 | Rotten Tomatoes - Best of 2020: 226 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2020 227 | sync_mode: sync 228 | collection_mode: default 229 | collection_order: custom 230 | sort_title: "*113" 231 | schedule: yearly(01/30) 232 | Rotten Tomatoes - Best of 2021: 233 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2021 234 | sync_mode: sync 235 | collection_mode: default 236 | collection_order: custom 237 | sort_title: "*113a" 238 | schedule: yearly(01/30) 239 | Rotten Tomatoes - Best of 2022: 240 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2022 241 | sync_mode: sync 242 | collection_mode: default 243 | collection_order: custom 244 | sort_title: "*113b" 245 | schedule: yearly(01/30) 246 | Rotten Tomatoes - Best of 2023: 247 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2023 248 | sync_mode: sync 249 | collection_mode: default 250 | collection_order: custom 251 | sort_title: "*113c" 252 | schedule: yearly(01/30) 253 | Rotten Tomatoes - Best of 2024: 254 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2024 255 | sync_mode: sync 256 | collection_mode: default 257 | collection_order: custom 258 | sort_title: "*113d" 259 | schedule: yearly(01/30) 260 | Rotten Tomatoes - Best of 2025: 261 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2025 262 | sync_mode: sync 263 | collection_mode: default 264 | collection_order: custom 265 | sort_title: "*113e" 266 | schedule: yearly(01/30) 267 | Rotten Tomatoes - Best All Time: 268 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-top-100-movies-of-all-time 269 | sync_mode: sync 270 | collection_mode: default 271 | collection_order: custom 272 | sort_title: "*114" 273 | schedule: monthly(10) 274 | Rotten Tomatoes - Best Rom Com: 275 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-the-200-best-romantic-comedies-of-all-time 276 | sync_mode: sync 277 | collection_mode: default 278 | collection_order: custom 279 | sort_title: "*115" 280 | schedule: monthly(10) 281 | Rotten Tomatoes - Best Horror: 282 | trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-horror-movies-of-all-time 283 | sync_mode: sync 284 | collection_mode: default 285 | collection_order: custom 286 | sort_title: "*116" 287 | schedule: monthly(10) 288 | Rotten Tomatoes - Best LGBT: 289 | trakt_list: https://trakt.tv/users/saara-v-a/lists/rotten-tomatoes-150-best-lgbt-movies-of-all-time-copy 290 | sync_mode: sync 291 | collection_mode: default 292 | collection_order: custom 293 | sort_title: "*117" 294 | schedule: monthly(10) 295 | 296 | ############################ 297 | # CUSTOM # 298 | ############################ 299 | 300 | Mindfuck: 301 | imdb_list: https://www.imdb.com/list/ls084006834 302 | sync_mode: sync 303 | collection_mode: default 304 | collection_order: alpha 305 | sort_title: "*400" 306 | summary: Movies that are mind bending, trippy, have big twists, have open endings, or make you think about them for days. Not all may apply. I have not watched them all. I gathered lists from multiple websites and Reddit posts. 307 | schedule: monthly(10) 308 | Mindfuck by LISH: 309 | trakt_list: https://trakt.tv/users/lish408/lists/mindfuck 310 | sync_mode: sync 311 | collection_mode: default 312 | collection_order: alpha 313 | sort_title: "*400b" 314 | summary: Complex plot. Big plot twist. Ending twist. 315 | schedule: monthly(10) 316 | Mindfuck by HD Movie Lists: 317 | mdblist_list: https://mdblist.com/lists/hdlists/mindfuck-movies 318 | sync_mode: sync 319 | collection_mode: default 320 | collection_order: alpha 321 | sort_title: "*400c" 322 | summary: A Growing Collection of Mindfuck Movies! 323 | schedule: monthly(10) 324 | James Bond: 325 | imdb_list: https://www.imdb.com/list/ls084710852 326 | sync_mode: sync 327 | collection_mode: default 328 | collection_order: release 329 | sort_title: "*401" 330 | schedule: monthly(10) 331 | Space Masterpieces: 332 | imdb_list: https://www.imdb.com/list/ls084547446 333 | sync_mode: sync 334 | collection_mode: default 335 | collection_order: alpha 336 | sort_title: "*402" 337 | schedule: monthly(10) 338 | Star Trek: 339 | imdb_list: https://www.imdb.com/list/ls084767125 340 | sync_mode: append # Append for Star Trek: Legacy 341 | collection_mode: default 342 | collection_order: release 343 | sort_title: "*403" 344 | schedule: monthly(10) 345 | Star Wars: 346 | imdb_list: https://www.imdb.com/list/ls084761910 347 | sync_mode: append # Append for Star Wars: The Mandalorian Parts I and II 348 | collection_mode: default 349 | collection_order: release 350 | sort_title: "*404" 351 | schedule: monthly(10) 352 | Rock Climbing: 353 | imdb_list: https://www.imdb.com/list/ls084590179 354 | sync_mode: append # Append for Dosage Collection. It got removed for some reason while on the IMDb list 355 | collection_mode: default 356 | collection_order: alpha 357 | sort_title: "*405" 358 | schedule: monthly(10) 359 | Abbott & Costello: 360 | imdb_list: https://www.imdb.com/list/ls084772233 361 | sync_mode: sync 362 | collection_mode: default 363 | collection_order: release 364 | sort_title: "*406" 365 | schedule: yearly(01/30) 366 | Inspirational & Heart Warming: 367 | imdb_list: 368 | - https://www.imdb.com/list/ls069754038 369 | - https://www.imdb.com/list/ls079599062 370 | - https://www.imdb.com/list/ls052687231 371 | tmdb_movie: 372 | - 227306 373 | - 14292 374 | - 14534 375 | sync_mode: sync 376 | collection_mode: default 377 | collection_order: alpha 378 | sort_title: "*407" 379 | schedule: monthly(10) 380 | True Stories: 381 | imdb_list: https://www.imdb.com/list/ls021398170 382 | sync_mode: sync 383 | collection_mode: default 384 | collection_order: alpha 385 | sort_title: "*408" 386 | schedule: monthly(10) 387 | Heists, Scams, Cons, and Robbers: 388 | imdb_list: 389 | - https://www.imdb.com/list/ls021415585 390 | - https://www.imdb.com/list/ls075742206 391 | - https://www.imdb.com/list/ls098660180 392 | - https://www.imdb.com/list/ls068224634 393 | - https://www.imdb.com/list/ls009794682 394 | - https://www.imdb.com/list/ls059928685 395 | sync_mode: sync 396 | collection_mode: default 397 | collection_order: alpha 398 | sort_title: "*409" 399 | schedule: monthly(10) 400 | Music: 401 | imdb_list: https://www.imdb.com/list/ls021457968 402 | tmdb_movie: 424694 403 | sync_mode: sync 404 | collection_mode: default 405 | collection_order: alpha 406 | sort_title: "*410" 407 | schedule: monthly(10) 408 | Spy & Agent: 409 | imdb_list: https://www.imdb.com/list/ls021488235 410 | tmdb_movie: 458156 411 | sync_mode: sync 412 | collection_mode: default 413 | collection_order: alpha 414 | sort_title: "*411" 415 | schedule: monthly(10) 416 | Prison: 417 | imdb_list: 418 | - https://www.imdb.com/list/ls021403323 419 | - https://www.imdb.com/list/ls055859064 420 | - https://www.imdb.com/list/ls053382995 421 | tmdb_movie: 422 | - 440471 423 | - 480042 424 | - 348893 425 | - 51620 426 | - 156717 427 | - 401478 428 | sync_mode: sync 429 | collection_mode: default 430 | collection_order: alpha 431 | sort_title: "*412" 432 | schedule: monthly(10) 433 | LGBT: 434 | plex_collection: Rotten Tomatoes - Best LGBT 435 | tmdb_movie: 436 | - 79837 437 | - 261776 438 | - 365126 439 | sync_mode: sync 440 | collection_mode: default 441 | collection_order: alpha 442 | sort_title: "*413" 443 | schedule: monthly(10) 444 | Time Travel: 445 | imdb_list: https://www.imdb.com/list/ls068617191 446 | sync_mode: sync 447 | collection_mode: default 448 | collection_order: alpha 449 | sort_title: "*414" 450 | summary: The ability to travel through time is by far my favorite movie story line. Who hasn"t, at least once in their lifetime, wished they could turn back the hands of time to buy a winning lottery ticket or to set something right that once went wrong? The movies listed below have a wide range of inventive ways on how the subjects are moved across time. Many of these films use either a mental ability, a magical device or time machine, some seem to have help from a higher power, and sometimes the person just wakes up in a different time. If you love TV shows like Outlander, Timeless, Doctor Who or Quantum Leap, then this list of the best time-traveling films is for you. -rlhron 451 | schedule: monthly(10) 452 | Natural Disasters: 453 | imdb_list: https://www.imdb.com/list/ls505219626 454 | tmdb_movie: 455 | - 646380 456 | sync_mode: sync 457 | collection_mode: default 458 | collection_order: alpha 459 | sort_title: "*415" 460 | schedule: monthly(10) 461 | Epidemics and Pandemics: 462 | imdb_list: https://www.imdb.com/list/ls505219442 463 | sync_mode: sync 464 | collection_mode: default 465 | collection_order: alpha 466 | sort_title: "*416" 467 | schedule: monthly(10) 468 | Unexpectedly Amazing: 469 | imdb_list: https://www.imdb.com/list/ls574663351 470 | sync_mode: sync 471 | collection_mode: default 472 | collection_order: alpha 473 | sort_title: "*417" 474 | summary: List gathered by Crystal Ro at BuzzFeed from https://www.reddit.com/r/AskReddit/comments/7uzuxq/what_was_the_most_unexpectedly_amazing_movie/?sort=top 475 | schedule: yearly(01/30) 476 | Parody: 477 | imdb_list: https://www.imdb.com/list/ls088925450 478 | mdblist_list: https://mdblist.com/lists/hdlists/top-50-parody-movies-of-all-time 479 | sync_mode: sync 480 | collection_mode: default 481 | collection_order: alpha 482 | sort_title: "*418" 483 | schedule: monthly(10) 484 | Video Games (Live-action): 485 | mdblist_list: https://mdblist.com/lists/awesomeaustn/movies-based-on-video-games-live-action 486 | sync_mode: sync 487 | collection_mode: default 488 | collection_order: alpha 489 | sort_title: "*419" 490 | schedule: monthly(10) 491 | Cirque du Soleil: 492 | mdblist_list: https://mdblist.com/lists/awesomeaustn/cirque-du-soleil 493 | sync_mode: sync 494 | collection_mode: default 495 | collection_order: release 496 | sort_title: "*420" 497 | schedule: monthly(10) 498 | Historical Black Movies: 499 | imdb_list: https://www.imdb.com/list/ls560237349 500 | sync_mode: sync 501 | collection_mode: default 502 | collection_order: alpha 503 | sort_title: "*421" 504 | schedule: monthly(10) 505 | Classic Disney Animated Movies: 506 | imdb_list: https://www.imdb.com/list/ls088609910 507 | sync_mode: sync 508 | collection_mode: default 509 | collection_order: release 510 | schedule: yearly(01/30) 511 | Classic Sports Movies: 512 | imdb_list: https://www.imdb.com/list/ls088609451 513 | sync_mode: sync 514 | collection_mode: default 515 | collection_order: alpha 516 | schedule: yearly(01/30) 517 | Disney Channel Original Movies: 518 | imdb_list: https://www.imdb.com/list/ls560667605 519 | sync_mode: sync 520 | collection_mode: default 521 | collection_order: alpha 522 | schedule: yearly(01/30) 523 | Stand-up Comedy: 524 | imdb_list: https://www.imdb.com/list/ls088617302 525 | sync_mode: sync 526 | collection_mode: default 527 | collection_order: alpha 528 | schedule: monthly(10) 529 | # Stand-up Comedy: 530 | # imdb_list: https://www.imdb.com/list/ls041728102 531 | # sync_mode: sync 532 | # collection_mode: default 533 | # collection_order: alpha 534 | # schedule: monthly(10) 535 | X-Men: 536 | imdb_list: https://www.imdb.com/list/ls560205320 537 | sync_mode: append # Special Editions not included in the list (Rogue) 538 | collection_mode: default 539 | collection_order: alpha 540 | schedule: monthly(10) 541 | Disney Parks: 542 | imdb_list: https://www.imdb.com/list/ls560201046 543 | sync_mode: append # Append for Ultimate Walt Disney World. It got removed for some reason while on the IMDb list 544 | collection_mode: default 545 | collection_order: alpha 546 | schedule: monthly(10) 547 | My Favorite Movies: 548 | imdb_list: https://www.imdb.com/list/ls550502260 549 | sync_mode: sync 550 | collection_mode: default 551 | collection_order: alpha 552 | sort_title: "*450" 553 | schedule: monthly(10) 554 | 555 | ############################ 556 | # STUDIOS # 557 | ############################ 558 | 559 | Pixar: 560 | imdb_list: https://www.imdb.com/list/ls084018331 561 | sync_mode: sync 562 | collection_mode: default 563 | collection_order: release 564 | sort_title: "*500" 565 | schedule: weekly(friday) 566 | Pixar Shorts: 567 | imdb_list: https://www.imdb.com/list/ls084018915 568 | sync_mode: sync 569 | collection_mode: default 570 | collection_order: release 571 | sort_title: "*501" 572 | schedule: weekly(friday) 573 | Pixar SparkShorts: 574 | tmdb_list: https://www.themoviedb.org/list/7107731 575 | sync_mode: sync 576 | collection_mode: default 577 | collection_order: release 578 | sort_title: "*502" 579 | summary: SparkShorts is a series of American independent animated short films produced by Pixar Animation Studios. It consists of a program in which Pixar's employees are given six months and limited budgets to develop animated short films that were originally released on Pixar's YouTube channel, and later on Disney+. 580 | schedule: weekly(friday) 581 | Marvel: 582 | imdb_list: https://www.imdb.com/list/ls084048577 583 | sync_mode: append # Append for Marvel's Infinity Saga: The Sacred Timeline 584 | collection_mode: default 585 | collection_order: release 586 | sort_title: "*503" 587 | schedule: weekly(friday) 588 | Marvel Cinematic Universe: 589 | imdb_list: https://www.imdb.com/list/ls084049253 590 | sync_mode: append # Append for Marvel's Infinity Saga: The Sacred Timeline 591 | collection_mode: default 592 | collection_order: release 593 | sort_title: "*504" 594 | schedule: weekly(friday) 595 | Marvel Cinematic Universe (Chronological): 596 | trakt_list: https://trakt.tv/users/jawann2002/lists/marvel-cinematic-universe-movies?sort=rank,asc 597 | sync_mode: append # Append for Marvel's Infinity Saga: The Sacred Timeline 598 | collection_mode: default 599 | collection_order: custom 600 | sort_title: "*504b" 601 | schedule: weekly(friday) 602 | file_poster: /config/assets/Marvel Cinematic Universe.png 603 | # Marvel Cinematic Universe (Chronological): # This includes the TV Shows also 604 | # trakt_list: https://trakt.tv/users/donxy/lists/marvel-cinematic-universe?sort=rank,asc 605 | # sync_mode: append # Append for Marvel's Infinity Saga: The Sacred Timeline 606 | # collection_mode: default 607 | # collection_order: custom 608 | # sort_title: "*504b" 609 | # schedule: weekly(friday) 610 | # file_poster: /config/assets/Marvel Cinematic Universe.png 611 | # DC: 612 | # tmdb_company: 429 613 | # sync_mode: sync 614 | # collection_mode: default 615 | # collection_order: release 616 | # sort_title: "*505" 617 | # file_poster: /config/assets/DC-2.png 618 | # schedule: weekly(friday) 619 | # DC: 620 | # plex_collection: 621 | # - DC Animated Movies 622 | # - DC Live Action 623 | # sync_mode: sync 624 | # collection_mode: default 625 | # collection_order: release 626 | # sort_title: "*505" 627 | # file_poster: /config/assets/DC-2.png 628 | # schedule: weekly(friday) 629 | DC Animated Movies: 630 | imdb_list: https://www.imdb.com/list/ls084123848 631 | sync_mode: sync 632 | collection_mode: default 633 | collection_order: release 634 | sort_title: "*506" 635 | schedule: weekly(friday) 636 | DC Animated Movie Universe: 637 | imdb_list: https://www.imdb.com/list/ls084121860 638 | sync_mode: sync 639 | collection_mode: default 640 | collection_order: release 641 | sort_title: "*507" 642 | schedule: weekly(friday) 643 | DC Showcase: 644 | imdb_list: https://www.imdb.com/list/ls084122453 645 | sync_mode: sync 646 | collection_mode: default 647 | collection_order: release 648 | sort_title: "*508" 649 | schedule: weekly(friday) 650 | DC Live Action: 651 | imdb_list: https://www.imdb.com/list/ls084122077 652 | sync_mode: sync 653 | collection_mode: default 654 | collection_order: release 655 | sort_title: "*509" 656 | schedule: weekly(friday) 657 | DC Extended Universe: 658 | imdb_list: https://www.imdb.com/list/ls084122673 659 | sync_mode: sync 660 | collection_mode: default 661 | collection_order: release 662 | sort_title: "*510" 663 | schedule: weekly(friday) 664 | Walt Disney Animation Studios: 665 | imdb_list: https://www.imdb.com/list/ls579363081 666 | sync_mode: sync 667 | collection_mode: default 668 | collection_order: release 669 | sort_title: "*511" 670 | schedule: weekly(friday) 671 | 672 | ############################ 673 | # NETWORKS # 674 | ############################ 675 | 676 | Apple TV+: 677 | imdb_list: https://www.imdb.com/list/ls084062897 678 | sync_mode: sync 679 | collection_mode: default 680 | collection_order: release 681 | sort_title: "*600" 682 | schedule: daily 683 | Netflix: 684 | imdb_list: https://www.imdb.com/list/ls084069524 685 | sync_mode: sync 686 | collection_mode: default 687 | collection_order: release 688 | sort_title: "*601" 689 | schedule: daily 690 | Netflix Stand-up: 691 | imdb_list: https://www.imdb.com/list/ls560028451 692 | sync_mode: sync 693 | collection_mode: default 694 | collection_order: release 695 | sort_title: "*602" 696 | schedule: daily 697 | Prime Video: 698 | imdb_list: https://www.imdb.com/list/ls084378896 699 | sync_mode: sync 700 | collection_mode: default 701 | collection_order: release 702 | sort_title: "*603" 703 | schedule: daily 704 | Hulu: 705 | imdb_list: https://www.imdb.com/list/ls084378943 706 | sync_mode: sync 707 | collection_mode: default 708 | collection_order: release 709 | sort_title: "*604" 710 | schedule: daily 711 | HBO Max: 712 | imdb_list: https://www.imdb.com/list/ls084378296 713 | sync_mode: sync 714 | collection_mode: default 715 | collection_order: release 716 | sort_title: "*605" 717 | schedule: daily 718 | Paramount+: 719 | imdb_list: https://www.imdb.com/list/ls574137821 720 | sync_mode: sync 721 | collection_mode: default 722 | collection_order: release 723 | sort_title: "*606" 724 | schedule: daily 725 | 726 | ############################ 727 | # HOLIDAYS # 728 | ############################ 729 | 730 | Halloween Movies: 731 | imdb_list: https://www.imdb.com/list/ls084064776 732 | sync_mode: sync 733 | collection_mode: default 734 | collection_order: alpha 735 | sort_title: "*700" 736 | schedule: yearly(09/30) 737 | Christmas Movies: 738 | imdb_list: https://www.imdb.com/list/ls084064285 739 | sync_mode: sync 740 | collection_mode: default 741 | collection_order: alpha 742 | sort_title: "*701" 743 | schedule: yearly(11/30) 744 | 745 | ############################ 746 | # GENRES # 747 | ############################ 748 | 749 | Comedy: 750 | plex_search: 751 | all: 752 | genre: Comedy 753 | sync_mode: sync 754 | collection_mode: default 755 | collection_order: alpha 756 | summary: A collection of Comedy movies 757 | sort_title: "*200" 758 | schedule: weekly(friday) 759 | "'80s Comedy": 760 | plex_search: 761 | all: 762 | decade: 1980 763 | genre: Comedy 764 | sync_mode: sync 765 | collection_mode: default 766 | collection_order: alpha 767 | summary: A collection of 80s Comedy movies 768 | sort_title: "*201" 769 | schedule: weekly(friday) 770 | "'90s Comedy": 771 | plex_search: 772 | all: 773 | decade: 1990 774 | genre: Comedy 775 | sync_mode: sync 776 | collection_mode: default 777 | collection_order: alpha 778 | summary: A collection of 90s Comedy movies 779 | sort_title: "*202" 780 | schedule: weekly(friday) 781 | Action: 782 | plex_search: 783 | all: 784 | genre: Action 785 | sync_mode: sync 786 | collection_mode: default 787 | collection_order: alpha 788 | summary: A collection of Action movies 789 | sort_title: "*204" 790 | schedule: weekly(friday) 791 | Drama: 792 | plex_search: 793 | all: 794 | genre: Drama 795 | sync_mode: sync 796 | collection_mode: default 797 | collection_order: alpha 798 | summary: A collection of Drama movies 799 | sort_title: "*205" 800 | schedule: weekly(friday) 801 | Documentary: 802 | plex_search: 803 | all: 804 | genre: Documentary 805 | sync_mode: sync 806 | collection_mode: default 807 | collection_order: alpha 808 | summary: A collection of Documentaries 809 | sort_title: "*206" 810 | schedule: weekly(friday) 811 | Romantic Comedy: 812 | plex_search: 813 | all: 814 | genres: Romance, Comedy 815 | sync_mode: sync 816 | collection_mode: default 817 | collection_order: alpha 818 | summary: A collection of Romantic Comedies 819 | sort_title: "*207" 820 | schedule: weekly(friday) 821 | Biography: 822 | plex_search: 823 | all: 824 | genre: Biography 825 | sync_mode: sync 826 | collection_mode: default 827 | collection_order: alpha 828 | summary: A collection of Biographies 829 | sort_title: "*208" 830 | schedule: weekly(friday) 831 | Mystery: 832 | plex_search: 833 | all: 834 | genre: Mystery 835 | sync_mode: sync 836 | collection_mode: default 837 | collection_order: alpha 838 | summary: A collection of Mysteries 839 | sort_title: "*209" 840 | schedule: weekly(friday) 841 | Crime: 842 | plex_search: 843 | all: 844 | genre: Crime 845 | sync_mode: sync 846 | collection_mode: default 847 | collection_order: alpha 848 | summary: A collection of Crime movies 849 | sort_title: "*210" 850 | schedule: weekly(friday) 851 | Science Fiction: 852 | plex_search: 853 | all: 854 | genre: Science Fiction 855 | sync_mode: sync 856 | collection_mode: default 857 | collection_order: alpha 858 | summary: A collection of Science Fiction movies 859 | sort_title: "*211" 860 | schedule: weekly(friday) 861 | Animation: 862 | plex_search: 863 | all: 864 | genre: Animation 865 | sync_mode: sync 866 | collection_mode: default 867 | collection_order: alpha 868 | summary: A collection of Animated movies 869 | sort_title: "*212" 870 | schedule: weekly(friday) 871 | 872 | ############################ 873 | # YEARS # 874 | ############################ 875 | 876 | 2020 Movies: 877 | plex_search: 878 | all: 879 | year: 2020 880 | sync_mode: sync 881 | collection_mode: default 882 | collection_order: release 883 | summary: A collection of movies in 2020 884 | sort_title: "*300" 885 | schedule: weekly(friday) 886 | 2021 Movies: 887 | plex_search: 888 | all: 889 | year: 2021 890 | sync_mode: sync 891 | collection_mode: default 892 | collection_order: release 893 | summary: A collection of movies in 2021 894 | sort_title: "*301" 895 | schedule: weekly(friday) 896 | 2022 Movies: 897 | plex_search: 898 | all: 899 | year: 2022 900 | sync_mode: sync 901 | collection_mode: default 902 | collection_order: release 903 | summary: A collection of movies in 2022 904 | sort_title: "*302" 905 | schedule: weekly(friday) 906 | 2023 Movies: 907 | plex_search: 908 | all: 909 | year: 2023 910 | sync_mode: sync 911 | collection_mode: default 912 | collection_order: release 913 | summary: A collection of movies in 2023 914 | sort_title: "*303" 915 | schedule: weekly(friday) 916 | 2024 Movies: 917 | plex_search: 918 | all: 919 | year: 2024 920 | sync_mode: sync 921 | collection_mode: default 922 | collection_order: release 923 | summary: A collection of movies in 2024 924 | sort_title: "*304" 925 | schedule: daily 926 | 2025 Movies: 927 | plex_search: 928 | all: 929 | year: 2025 930 | sync_mode: sync 931 | collection_mode: default 932 | collection_order: release 933 | summary: A collection of movies in 2025 934 | sort_title: "*305" 935 | schedule: daily 936 | 937 | ############################ 938 | # AUDIO # 939 | ############################ 940 | 941 | TrueHD Atmos: 942 | plex_all: true 943 | filters: 944 | filepath: TrueHD Atmos 945 | sync_mode: sync 946 | collection_mode: default 947 | collection_order: alpha 948 | sort_title: "*750" 949 | schedule: daily 950 | DTS-HD Master Audio: 951 | plex_all: true 952 | filters: 953 | filepath: DTS-HD MA 954 | sync_mode: sync 955 | collection_mode: default 956 | collection_order: alpha 957 | sort_title: "*751" 958 | schedule: daily 959 | Dolby Atmos: 960 | plex_all: true 961 | filters: 962 | audio_codec: truehd 963 | audio_profile: atmos 964 | sync_mode: sync 965 | collection_mode: default 966 | collection_order: alpha 967 | sort_title: "*752" 968 | schedule: daily 969 | 970 | ############################ 971 | # ACTORS # 972 | ############################ 973 | 974 | Dwayne Johnson: 975 | tmdb_actor: 18918 976 | sync_mode: sync 977 | collection_mode: default 978 | collection_order: release 979 | sort_title: "*800" 980 | schedule: weekly(friday) 981 | Leonardo DiCaprio: 982 | tmdb_actor: 6193 983 | sync_mode: sync 984 | collection_mode: default 985 | collection_order: release 986 | sort_title: "*801" 987 | schedule: weekly(friday) 988 | Keanu Reeves: 989 | tmdb_actor: 6384 990 | sync_mode: sync 991 | collection_mode: default 992 | collection_order: release 993 | sort_title: "*802" 994 | schedule: weekly(friday) 995 | Morgan Freeman: 996 | tmdb_actor: 192 997 | sync_mode: sync 998 | collection_mode: default 999 | collection_order: release 1000 | sort_title: "*803" 1001 | schedule: weekly(friday) 1002 | Scarlett Johansson: 1003 | tmdb_actor: 1245 1004 | sync_mode: sync 1005 | collection_mode: default 1006 | collection_order: release 1007 | sort_title: "*850" 1008 | schedule: weekly(friday) 1009 | Gal Gadot: 1010 | tmdb_actor: 90633 1011 | sync_mode: sync 1012 | collection_mode: default 1013 | collection_order: release 1014 | sort_title: "*851" 1015 | schedule: weekly(friday) 1016 | Charlize Theron: 1017 | tmdb_actor: 6885 1018 | sync_mode: sync 1019 | collection_mode: default 1020 | collection_order: release 1021 | sort_title: "*852" 1022 | schedule: weekly(friday) 1023 | 1024 | ############################ 1025 | # DIRECTORS # 1026 | ############################ 1027 | 1028 | Christopher Nolan: 1029 | tmdb_director: 525 1030 | sync_mode: sync 1031 | collection_mode: default 1032 | collection_order: release 1033 | sort_title: "*900" 1034 | schedule: weekly(friday) 1035 | Quentin Tarantino: 1036 | tmdb_director: 138 1037 | sync_mode: sync 1038 | collection_mode: default 1039 | collection_order: release 1040 | sort_title: "*901" 1041 | schedule: weekly(friday) 1042 | Clint Eastwood: 1043 | tmdb_director: 190 1044 | tmdb_writer: 190 1045 | tmdb_actor: 190 1046 | sync_mode: sync 1047 | collection_mode: default 1048 | collection_order: release 1049 | sort_title: "*902" 1050 | schedule: weekly(friday) 1051 | 1052 | ############################ 1053 | # HOLIDAYS BY DRAZZIZZI # 1054 | ############################ 1055 | 1056 | Valentines Day Movies: 1057 | schedule: range(02/01-02/14) 1058 | template: { name: Holiday, holiday: "Valentine's Day" } 1059 | imdb_list: 1060 | - https://www.imdb.com/list/ls000094398 1061 | - https://www.imdb.com/list/ls057783436 1062 | - https://www.imdb.com/list/ls064427905 1063 | collection_order: release 1064 | St. Patricks Day Movies: 1065 | schedule: range(03/01-03/17) 1066 | template: { name: Holiday, holiday: "St. Patrick's Day" } 1067 | imdb_list: https://www.imdb.com/list/ls063934595 1068 | collection_order: release 1069 | Thanksgiving Movies: 1070 | schedule: range(10/01-10/14) 1071 | sort_title: "!AA" 1072 | template: { name: Holiday, holiday: Thanksgiving } 1073 | imdb_list: 1074 | - https://www.imdb.com/list/ls000835734 1075 | - https://www.imdb.com/list/ls091597850 1076 | collection_order: release 1077 | Halloween Movies (Big List): # Named Big List because I already have one named Halloween Movies 1078 | schedule: range(10/01-10/31) 1079 | sort_title: "!AB" 1080 | template: { name: Holiday, holiday: Halloween } 1081 | imdb_list: 1082 | - https://www.imdb.com/list/ls023118929 1083 | - https://www.imdb.com/list/ls000099714 1084 | - https://www.imdb.com/list/ls000058693 1085 | - https://www.imdb.com/search/title/?genres=horror&keywords=haunted-house&sort=moviemeter,asc 1086 | tmdb_collection: 1087 | - 91361 # Halloween Collection 1088 | - 8581 # A Nightmare on Elm Street Collection 1089 | - 1733 # The Mummy Collection 1090 | - 8091 # Alien Collection 1091 | tmdb_movie: 1092 | - 23437 # A Nightmare on Elm Street (2010) 1093 | collection_order: release 1094 | Christmas Movies (Big List): # Named Big List because I already have one named Christmas Movies 1095 | schedule: range(12/01-12/31) 1096 | sort_title: "!AB" 1097 | template: { name: Holiday, holiday: Christmas } 1098 | imdb_list: 1099 | - https://www.imdb.com/list/ls000096828 1100 | - https://www.imdb.com/list/ls097394442 1101 | - https://www.imdb.com/list/ls068976997 1102 | - https://www.imdb.com/list/ls027567380 1103 | collection_order: release 1104 | New Years Eve Movies: 1105 | schedule: range(12/26-01/05) 1106 | sort_title: "!AA" 1107 | template: { name: Holiday, holiday: "New Year's Eve" } 1108 | imdb_list: https://www.imdb.com/list/ls066838460 1109 | 1110 | ############################ 1111 | # LOCATION # 1112 | ############################ 1113 | 1114 | # Arizona made by tikilab 1115 | 1116 | Arizona: 1117 | imdb_list: https://www.imdb.com/search/title/?title_type=feature&locations=arizona 1118 | sync_mode: sync 1119 | collection_mode: default 1120 | collection_order: alpha 1121 | sort_title: Arizona 1122 | summary: Films shot in Arizona. 1123 | schedule: weekly(friday) 1124 | 1125 | ############################ 1126 | # DOLBY # 1127 | ############################ 1128 | 1129 | # Dolby Atmos: 1130 | # mdblist_list: https://mdblist.com/lists/blisskodi/dolby-atmos-film 1131 | # sync_mode: sync 1132 | # collection_mode: default 1133 | # collection_order: custom 1134 | # sort_title: "*950" 1135 | # schedule: weekly(friday) 1136 | # The default sort_by when it's not specified is score.desc 1137 | # Dolby Vision: 1138 | # mdblist_list: https://mdblist.com/lists/blisskodi/dolby-vision-film 1139 | # sync_mode: sync 1140 | # collection_mode: default 1141 | # collection_order: custom 1142 | # sort_title: "*951" 1143 | # schedule: weekly(friday) 1144 | # The default sort_by when it's not specified is score.desc 1145 | # Dolby Atmos & Dolby Vision: 1146 | # mdblist_list: https://mdblist.com/lists/blisskodi/dolby-atmos-dolby-vision-film 1147 | # sync_mode: sync 1148 | # collection_mode: default 1149 | # collection_order: custom 1150 | # sort_title: "*952" 1151 | # schedule: weekly(friday) 1152 | # The default sort_by when it's not specified is score.desc 1153 | 1154 | ############################ 1155 | # MOVIE OF THE DAY # 1156 | ############################ 1157 | 1158 | # Made by overlord 1159 | 1160 | # Remove Label: 1161 | # build_collection: false 1162 | # plex_all: true # or whatever kinda of search you want 1163 | # label.remove: Movie Day Remove 1164 | 1165 | Movie of the Day: 1166 | sort_title: zzzz_MovieDay 1167 | smart_label: random 1168 | sync_mode: sync 1169 | plex_search: 1170 | limit: 1 1171 | all: 1172 | year.gte: 1970 1173 | genre.not: documentary 1174 | critic_rating.gte: 7.0 1175 | audience_rating.gte: 8.0 1176 | label.not: Movie of the Day Remove 1177 | filters: 1178 | has_collection: false 1179 | summary: Randomly picked and highly rated! Excludes the MCU, Harry Potter, X-Men, and so on. Also excludes movies that have prequels or sequels. 1180 | visible_library: true 1181 | visible_home: true 1182 | visible_shared: true 1183 | 1184 | Movie of the Day Remove: 1185 | sort_title: zzzz_MovieDayRemove 1186 | smart_label: random 1187 | sync_mode: append 1188 | plex_search: 1189 | limit: 25000 1190 | all: 1191 | year.gte: 1970 1192 | genre.not: documentary 1193 | critic_rating.gte: 7.0 1194 | audience_rating.gte: 8.0 1195 | label: Movie of the Day 1196 | summary: Movies featured on "Movie of the Day" collection will be put into this collection to not be considered again. 1197 | -------------------------------------------------------------------------------- /plex_meta_manager.py: -------------------------------------------------------------------------------- 1 | ## THIS IS NOT USED, IT IS HERE FOR REFERENCE 2 | 3 | from modules.util import Failed, FilterFailed, NonExisting, NotScheduled, Deleted 4 | from modules.config import ConfigFile 5 | from modules.builder import CollectionBuilder 6 | from modules import util 7 | import argparse 8 | import os 9 | import platform 10 | import psutil 11 | import sys 12 | import time 13 | import uuid 14 | from collections import Counter 15 | from concurrent.futures import ProcessPoolExecutor 16 | from datetime import datetime 17 | from modules.logs import MyLogger 18 | 19 | try: 20 | import plexapi 21 | import requests 22 | import schedule 23 | from PIL import ImageFile 24 | from plexapi import server 25 | from plexapi.exceptions import NotFound 26 | from plexapi.video import Show, Season 27 | except ModuleNotFoundError: 28 | print("Requirements Error: Requirements are not installed") 29 | sys.exit(0) 30 | 31 | if sys.version_info[0] != 3 or sys.version_info[1] < 7: 32 | print("Version Error: Version: %s.%s.%s incompatible please use Python 3.7+" % 33 | (sys.version_info[0], sys.version_info[1], sys.version_info[2])) 34 | sys.exit(0) 35 | 36 | parser = argparse.ArgumentParser() 37 | parser.add_argument("-db", "--debug", dest="debug", 38 | help="Run with Debug Logs Reporting to the Command Window", action="store_true", default=False) 39 | parser.add_argument("-tr", "--trace", dest="trace", 40 | help="Run with extra Trace Debug Logs", action="store_true", default=False) 41 | parser.add_argument("-c", "--config", dest="config", 42 | help="Run with desired *.yml file", type=str) 43 | parser.add_argument("-t", "--time", "--times", dest="times", 44 | help="Times to update each day use format HH:MM (Default: 05:00) (comma-separated list)", default="05:00", type=str) 45 | parser.add_argument("-ti", "--timeout", dest="timeout", 46 | help="PMM Global Timeout (Default: 180)", default=180, type=int) 47 | parser.add_argument("-re", "--resume", dest="resume", 48 | help="Resume collection run from a specific collection", type=str) 49 | parser.add_argument("-r", "--run", dest="run", 50 | help="Run without the scheduler", action="store_true", default=False) 51 | parser.add_argument("-is", "--ignore-schedules", dest="ignore_schedules", 52 | help="Run ignoring collection schedules", action="store_true", default=False) 53 | parser.add_argument("-ig", "--ignore-ghost", dest="ignore_ghost", 54 | help="Run ignoring ghost logging", action="store_true", default=False) 55 | parser.add_argument("-rt", "--test", "--tests", "--run-test", "--run-tests", dest="test", 56 | help="Run in debug mode with only collections that have test: true", action="store_true", default=False) 57 | parser.add_argument("-co", "--collection-only", "--collections-only", dest="collection_only", 58 | help="Run only collection operations", action="store_true", default=False) 59 | parser.add_argument("-po", "--playlist-only", "--playlists-only", dest="playlist_only", 60 | help="Run only playlist operations", action="store_true", default=False) 61 | parser.add_argument("-op", "--operation", "--operations", "-lo", "--library-only", "--libraries-only", "--operation-only", 62 | "--operations-only", dest="operations", help="Run only operations", action="store_true", default=False) 63 | parser.add_argument("-ov", "--overlay", "--overlays", "--overlay-only", "--overlays-only", 64 | dest="overlays", help="Run only overlays", action="store_true", default=False) 65 | parser.add_argument("-lf", "--library-first", "--libraries-first", dest="library_first", 66 | help="Run library operations before collections", action="store_true", default=False) 67 | parser.add_argument("-rc", "-cl", "--collection", "--collections", "--run-collection", "--run-collections", 68 | dest="collections", help="Process only specified collections (comma-separated list)", type=str) 69 | parser.add_argument("-rl", "-l", "--library", "--libraries", "--run-library", "--run-libraries", 70 | dest="libraries", help="Process only specified libraries (comma-separated list)", type=str) 71 | parser.add_argument("-rm", "-m", "--metadata", "--metadata-files", "--run-metadata-files", 72 | dest="metadata", help="Process only specified Metadata files (comma-separated list)", type=str) 73 | parser.add_argument("-ca", "--cache-library", "--cache-libraries", dest="cache_libraries", 74 | help="Cache Library load for 1 day", action="store_true", default=False) 75 | parser.add_argument("-dc", "--delete", "--delete-collections", dest="delete_collections", 76 | help="Deletes all Collections in the Plex Library before running", action="store_true", default=False) 77 | parser.add_argument("-dl", "--delete-label", "--delete-labels", dest="delete_labels", 78 | help="Deletes all Labels in the Plex Library before running", action="store_true", default=False) 79 | parser.add_argument("-nc", "--no-countdown", dest="no_countdown", 80 | help="Run without displaying the countdown", action="store_true", default=False) 81 | parser.add_argument("-nm", "--no-missing", dest="no_missing", 82 | help="Run without running the missing section", action="store_true", default=False) 83 | parser.add_argument("-nr", "--no-report", dest="no_report", 84 | help="Run without saving a report", action="store_true", default=False) 85 | parser.add_argument("-ro", "--read-only-config", dest="read_only_config", 86 | help="Run without writing to the config", action="store_true", default=False) 87 | parser.add_argument("-pu", "--plex-url", dest="plex_url", 88 | help="Plex URL for Plex ENV URLs", default="", type=str) 89 | parser.add_argument("-pt", "--plex-token", dest="plex_token", 90 | help="Plex Token for Plex ENV Tokens", default="", type=str) 91 | parser.add_argument("-d", "--divider", dest="divider", 92 | help="Character that divides the sections (Default: '=')", default="=", type=str) 93 | parser.add_argument("-w", "--width", dest="width", 94 | help="Screen Width (Default: 100)", default=100, type=int) 95 | args = parser.parse_args() 96 | 97 | 98 | def get_arg(env_str, default, arg_bool=False, arg_int=False): 99 | env_vars = [env_str] if not isinstance(env_str, list) else env_str 100 | final_value = None 101 | for env_var in env_vars: 102 | env_value = os.environ.get(env_var) 103 | if env_value is not None: 104 | final_value = env_value 105 | break 106 | if final_value or (arg_int and final_value == 0): 107 | if arg_bool: 108 | if final_value is True or final_value is False: 109 | return final_value 110 | elif final_value.lower() in ["t", "true"]: 111 | return True 112 | else: 113 | return False 114 | elif arg_int: 115 | try: 116 | return int(final_value) 117 | except ValueError: 118 | return default 119 | else: 120 | return str(final_value) 121 | else: 122 | return default 123 | 124 | 125 | is_docker = get_arg("PMM_DOCKER", False, arg_bool=True) 126 | is_linuxserver = get_arg("PMM_LINUXSERVER", False, arg_bool=True) 127 | run_arg = " ".join([f'"{s}"' if " " in s else s for s in sys.argv[:]]) 128 | config_file = get_arg("PMM_CONFIG", args.config) 129 | times = get_arg("PMM_TIME", args.times) 130 | run = get_arg("PMM_RUN", args.run, arg_bool=True) 131 | test = get_arg("PMM_TEST", args.test, arg_bool=True) 132 | ignore_schedules = get_arg("PMM_IGNORE_SCHEDULES", 133 | args.ignore_schedules, arg_bool=True) 134 | ignore_ghost = get_arg("PMM_IGNORE_GHOST", args.ignore_ghost, arg_bool=True) 135 | collection_only = get_arg("PMM_COLLECTIONS_ONLY", 136 | args.collection_only, arg_bool=True) 137 | playlist_only = get_arg("PMM_PLAYLISTS_ONLY", 138 | args.playlist_only, arg_bool=True) 139 | operations_only = get_arg(["PMM_OPERATIONS", "PMM_OPERATIONS_ONLY", 140 | "PMM_LIBRARIES_ONLY"], args.operations, arg_bool=True) 141 | overlays_only = get_arg( 142 | ["PMM_OVERLAYS", "PMM_OVERLAYS_ONLY"], args.overlays, arg_bool=True) 143 | library_first = get_arg("PMM_LIBRARIES_FIRST", 144 | args.library_first, arg_bool=True) 145 | collections = get_arg("PMM_COLLECTIONS", args.collections) 146 | libraries = get_arg("PMM_LIBRARIES", args.libraries) 147 | metadata_files = get_arg("PMM_METADATA_FILES", args.metadata) 148 | cache_libraries = get_arg("PMM_CACHE_LIBRARIES", 149 | args.cache_libraries, arg_bool=True) 150 | delete_collections = get_arg( 151 | "PMM_DELETE_COLLECTIONS", args.delete_collections, arg_bool=True) 152 | delete_labels = get_arg("PMM_DELETE_LABELS", args.delete_labels, arg_bool=True) 153 | resume = get_arg("PMM_RESUME", args.resume) 154 | no_countdown = get_arg("PMM_NO_COUNTDOWN", args.no_countdown, arg_bool=True) 155 | no_missing = get_arg("PMM_NO_MISSING", args.no_missing, arg_bool=True) 156 | no_report = get_arg("PMM_NO_REPORT", args.no_report, arg_bool=True) 157 | read_only_config = get_arg("PMM_READ_ONLY_CONFIG", 158 | args.read_only_config, arg_bool=True) 159 | divider = get_arg("PMM_DIVIDER", args.divider) 160 | screen_width = get_arg("PMM_WIDTH", args.width, arg_int=True) 161 | timeout = get_arg("PMM_TIMEOUT", args.timeout, arg_int=True) 162 | debug = get_arg("PMM_DEBUG", args.debug, arg_bool=True) 163 | trace = get_arg("PMM_TRACE", args.trace, arg_bool=True) 164 | plex_url = get_arg("PMM_PLEX_URL", args.plex_url) 165 | plex_token = get_arg("PMM_PLEX_TOKEN", args.plex_token) 166 | 167 | if collections: 168 | collection_only = True 169 | 170 | if screen_width < 90 or screen_width > 300: 171 | print( 172 | f"Argument Error: width argument invalid: {screen_width} must be an integer between 90 and 300 using the default 100") 173 | screen_width = 100 174 | 175 | default_dir = os.path.join(os.path.dirname( 176 | os.path.abspath(__file__)), "config") 177 | if config_file and os.path.exists(config_file): 178 | default_dir = os.path.join(os.path.dirname(os.path.abspath(config_file))) 179 | elif config_file and not os.path.exists(config_file): 180 | print(f"Config Error: config not found at {os.path.abspath(config_file)}") 181 | sys.exit(0) 182 | elif not os.path.exists(os.path.join(default_dir, "config.yml")): 183 | print(f"Config Error: config not found at {os.path.abspath(default_dir)}") 184 | sys.exit(0) 185 | 186 | logger = MyLogger("Plex Meta Manager", default_dir, screen_width, 187 | divider[0], ignore_ghost, test or debug, trace) 188 | 189 | util.logger = logger 190 | 191 | 192 | def my_except_hook(exctype, value, tb): 193 | if issubclass(exctype, KeyboardInterrupt): 194 | sys.__excepthook__(exctype, value, tb) 195 | else: 196 | logger.critical("Uncaught Exception", exc_info=(exctype, value, tb)) 197 | 198 | 199 | sys.excepthook = my_except_hook 200 | 201 | old_send = requests.Session.send 202 | 203 | 204 | def new_send(*send_args, **kwargs): 205 | if kwargs.get("timeout", None) is None: 206 | kwargs["timeout"] = timeout 207 | return old_send(*send_args, **kwargs) 208 | 209 | 210 | requests.Session.send = new_send 211 | 212 | version = ("Unknown", "Unknown", 0) 213 | with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION")) as handle: 214 | for line in handle.readlines(): 215 | line = line.strip() 216 | if len(line) > 0: 217 | version = util.parse_version(line) 218 | break 219 | 220 | uuid_file = os.path.join(default_dir, "UUID") 221 | uuid_num = None 222 | if os.path.exists(uuid_file): 223 | with open(uuid_file) as handle: 224 | for line in handle.readlines(): 225 | line = line.strip() 226 | if len(line) > 0: 227 | uuid_num = line 228 | break 229 | if not uuid_num: 230 | uuid_num = uuid.uuid4() 231 | with open(uuid_file, "w") as handle: 232 | handle.write(str(uuid_num)) 233 | 234 | plexapi.BASE_HEADERS["X-Plex-Client-Identifier"] = str(uuid_num) 235 | ImageFile.LOAD_TRUNCATED_IMAGES = True 236 | 237 | 238 | def process(attrs): 239 | with ProcessPoolExecutor(max_workers=1) as executor: 240 | executor.submit(start, *[attrs]) 241 | 242 | 243 | def start(attrs): 244 | logger.add_main_handler() 245 | logger.separator() 246 | logger.info("") 247 | logger.info_center( 248 | " ____ _ __ __ _ __ __ ") 249 | logger.info_center( 250 | "| _ \\| | _____ __ | \\/ | ___| |_ __ _ | \\/ | __ _ _ __ __ _ __ _ ___ _ __ ") 251 | logger.info_center( 252 | "| |_) | |/ _ \\ \\/ / | |\\/| |/ _ \\ __/ _` | | |\\/| |/ _` | '_ \\ / _` |/ _` |/ _ \\ '__|") 253 | logger.info_center( 254 | "| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ") 255 | logger.info_center( 256 | "|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ") 257 | logger.info_center( 258 | " |___/ ") 259 | system_ver = "Docker" if is_docker else "Linuxserver" if is_linuxserver else f"Python {platform.python_version()}" 260 | logger.info(f" Version: {version[0]} ({system_ver})") 261 | latest_version = util.current_version(version) 262 | new_version = latest_version[0] if latest_version and ( 263 | version[1] != latest_version[1] or (version[2] and version[2] < latest_version[2])) else None 264 | if new_version: 265 | logger.info(f" Newest Version: {new_version}") 266 | logger.info(f" Platform: {platform.platform()}") 267 | logger.info( 268 | f" Memory: {round(psutil.virtual_memory().total / (1024.0 ** 3))} GB") 269 | if "time" in attrs and attrs["time"]: 270 | start_type = f"{attrs['time']} " 271 | elif "test" in attrs and attrs["test"]: 272 | start_type = "Test " 273 | elif "collections" in attrs and attrs["collections"]: 274 | start_type = "Collections " 275 | elif "libraries" in attrs and attrs["libraries"]: 276 | start_type = "Libraries " 277 | else: 278 | start_type = "" 279 | start_time = datetime.now() 280 | if "time" not in attrs: 281 | attrs["time"] = start_time.strftime("%H:%M") 282 | attrs["time_obj"] = start_time 283 | attrs["read_only"] = read_only_config 284 | attrs["version"] = version 285 | attrs["no_missing"] = no_missing 286 | attrs["no_report"] = no_report 287 | attrs["collection_only"] = collection_only 288 | attrs["playlist_only"] = playlist_only 289 | attrs["operations_only"] = operations_only 290 | attrs["overlays_only"] = overlays_only 291 | attrs["plex_url"] = plex_url 292 | attrs["plex_token"] = plex_token 293 | logger.separator(debug=True) 294 | logger.debug(f"Run Command: {run_arg}") 295 | logger.debug(f"--config (PMM_CONFIG): {config_file}") 296 | logger.debug(f"--time (PMM_TIME): {times}") 297 | logger.debug(f"--run (PMM_RUN): {run}") 298 | logger.debug(f"--run-tests (PMM_TEST): {test}") 299 | logger.debug( 300 | f"--collections-only (PMM_COLLECTIONS_ONLY): {collection_only}") 301 | logger.debug(f"--playlists-only (PMM_PLAYLISTS_ONLY): {playlist_only}") 302 | logger.debug(f"--operations (PMM_OPERATIONS): {operations_only}") 303 | logger.debug(f"--overlays (PMM_OVERLAYS): {overlays_only}") 304 | logger.debug(f"--libraries-first (PMM_LIBRARIES_FIRST): {library_first}") 305 | logger.debug(f"--run-collections (PMM_COLLECTIONS): {collections}") 306 | logger.debug(f"--run-libraries (PMM_LIBRARIES): {libraries}") 307 | logger.debug( 308 | f"--run-metadata-files (PMM_METADATA_FILES): {metadata_files}") 309 | logger.debug( 310 | f"--ignore-schedules (PMM_IGNORE_SCHEDULES): {ignore_schedules}") 311 | logger.debug(f"--ignore-ghost (PMM_IGNORE_GHOST): {ignore_ghost}") 312 | logger.debug(f"--cache-libraries (PMM_CACHE_LIBRARIES): {cache_libraries}") 313 | logger.debug( 314 | f"--delete-collections (PMM_DELETE_COLLECTIONS): {delete_collections}") 315 | logger.debug(f"--delete-labels (PMM_DELETE_LABELS): {delete_labels}") 316 | logger.debug(f"--resume (PMM_RESUME): {resume}") 317 | logger.debug(f"--no-countdown (PMM_NO_COUNTDOWN): {no_countdown}") 318 | logger.debug(f"--no-missing (PMM_NO_MISSING): {no_missing}") 319 | logger.debug(f"--no-report (PMM_NO_REPORT): {no_report}") 320 | logger.debug( 321 | f"--read-only-config (PMM_READ_ONLY_CONFIG): {read_only_config}") 322 | logger.debug(f"--plex-url (PMM_PLEX_URL): {'Used' if plex_url else ''}") 323 | logger.debug( 324 | f"--plex-token (PMM_PLEX_TOKEN): {'Used' if plex_token else ''}") 325 | logger.debug(f"--divider (PMM_DIVIDER): {divider}") 326 | logger.debug(f"--width (PMM_WIDTH): {screen_width}") 327 | logger.debug(f"--debug (PMM_DEBUG): {debug}") 328 | logger.debug(f"--trace (PMM_TRACE): {trace}") 329 | logger.debug("") 330 | logger.separator(f"Starting {start_type}Run") 331 | config = None 332 | stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, 333 | "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0, "names": []} 334 | try: 335 | config = ConfigFile(default_dir, attrs) 336 | except Exception as e: 337 | logger.stacktrace() 338 | logger.critical(e) 339 | else: 340 | try: 341 | stats = run_config(config, stats) 342 | except Exception as e: 343 | config.notify(e) 344 | logger.stacktrace() 345 | logger.critical(e) 346 | logger.info("") 347 | end_time = datetime.now() 348 | run_time = str(end_time - start_time).split(".")[0] 349 | if config: 350 | try: 351 | config.Webhooks.end_time_hooks( 352 | start_time, end_time, run_time, stats) 353 | except Failed as e: 354 | logger.stacktrace() 355 | logger.error(f"Webhooks Error: {e}") 356 | version_line = f"Version: {version[0]}" 357 | if new_version: 358 | version_line = f"{version_line} Newest Version: {new_version}" 359 | try: 360 | log_data = {} 361 | with open(logger.main_log, encoding="utf-8") as f: 362 | for log_line in f: 363 | for err_type in ["WARNING", "ERROR", "CRITICAL"]: 364 | if f"[{err_type}]" in log_line: 365 | log_line = log_line.split("|")[1].strip() 366 | if err_type not in log_data: 367 | log_data[err_type] = [] 368 | log_data[err_type].append(log_line) 369 | 370 | for err_type in ["WARNING", "ERROR", "CRITICAL"]: 371 | if err_type not in log_data: 372 | continue 373 | logger.separator( 374 | f"{err_type.lower().capitalize()} Summary", space=False, border=False) 375 | 376 | logger.info("") 377 | logger.info("Count | Message") 378 | logger.separator(f"{logger.separating_character * 5}|", 379 | space=False, border=False, side_space=False, left=True) 380 | for k, v in Counter(log_data[err_type]).most_common(): 381 | logger.info(f"{v:>5} | {k}") 382 | logger.info("") 383 | except Failed as e: 384 | logger.stacktrace() 385 | logger.error(f"Report Error: {e}") 386 | 387 | logger.separator( 388 | f"Finished {start_type}Run\n{version_line}\nFinished: {end_time.strftime('%H:%M:%S %Y-%m-%d')} Run Time: {run_time}") 389 | logger.remove_main_handler() 390 | 391 | 392 | def run_config(config, stats): 393 | library_status = run_libraries(config) 394 | 395 | playlist_status = {} 396 | playlist_stats = {} 397 | if (config.playlist_files or config.general["playlist_report"]) and not overlays_only and not operations_only and not collection_only and not config.requested_metadata_files: 398 | logger.add_playlists_handler() 399 | if config.playlist_files: 400 | playlist_status, playlist_stats = run_playlists(config) 401 | if config.general["playlist_report"]: 402 | ran = [] 403 | for library in config.libraries: 404 | if library.PlexServer.machineIdentifier in ran: 405 | continue 406 | ran.append(library.PlexServer.machineIdentifier) 407 | logger.info("") 408 | logger.separator( 409 | f"{library.PlexServer.friendlyName} Playlist Report") 410 | logger.info("") 411 | report = library.playlist_report() 412 | max_length = 0 413 | for playlist_name in report: 414 | if len(playlist_name) > max_length: 415 | max_length = len(playlist_name) 416 | logger.info(f"{'Playlist Title':<{max_length}} | Users") 417 | logger.separator(f"{logger.separating_character * max_length}|", 418 | space=False, border=False, side_space=False, left=True) 419 | for playlist_name, users in report.items(): 420 | logger.info( 421 | f"{playlist_name:<{max_length}} | {'all' if len(users) == len(library.users) + 1 else ', '.join(users)}") 422 | logger.remove_playlists_handler() 423 | 424 | amount_added = 0 425 | if not operations_only and not overlays_only and not playlist_only: 426 | has_run_again = False 427 | for library in config.libraries: 428 | if library.run_again: 429 | has_run_again = True 430 | break 431 | 432 | if has_run_again: 433 | logger.info("") 434 | logger.separator("Run Again") 435 | logger.info("") 436 | for x in range(1, config.general["run_again_delay"] + 1): 437 | logger.ghost( 438 | f"Waiting to run again in {config.general['run_again_delay'] - x + 1} minutes") 439 | for y in range(60): 440 | time.sleep(1) 441 | logger.exorcise() 442 | for library in config.libraries: 443 | if library.run_again: 444 | try: 445 | logger.re_add_library_handler(library.mapping_name) 446 | os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str( 447 | library.timeout) 448 | logger.info("") 449 | logger.separator(f"{library.name} Library Run Again") 450 | logger.info("") 451 | library.map_guids(library.cache_items()) 452 | for builder in library.run_again: 453 | logger.info("") 454 | logger.separator( 455 | f"{builder.name} Collection in {library.name}") 456 | logger.info("") 457 | try: 458 | amount_added += builder.run_collections_again() 459 | except Failed as e: 460 | library.notify( 461 | e, collection=builder.name, critical=False) 462 | logger.stacktrace() 463 | logger.error(e) 464 | logger.remove_library_handler(library.mapping_name) 465 | except Exception as e: 466 | library.notify(e) 467 | logger.stacktrace() 468 | logger.critical(e) 469 | 470 | if not collection_only and not overlays_only and not playlist_only: 471 | used_url = [] 472 | for library in config.libraries: 473 | if library.url not in used_url: 474 | used_url.append(library.url) 475 | if library.empty_trash: 476 | library.query(library.PlexServer.library.emptyTrash) 477 | if library.clean_bundles: 478 | library.query(library.PlexServer.library.cleanBundles) 479 | if library.optimize: 480 | library.query(library.PlexServer.library.optimize) 481 | 482 | longest = 20 483 | for library in config.libraries: 484 | for title in library.status: 485 | if len(title) > longest: 486 | longest = len(title) 487 | if playlist_status: 488 | for title in playlist_status: 489 | if len(title) > longest: 490 | longest = len(title) 491 | 492 | def print_status(status): 493 | logger.info( 494 | f"{'Title':^{longest}} | + | = | - | Run Time | {'Status'}") 495 | breaker = f"{logger.separating_character * longest}|{logger.separating_character * 7}|{logger.separating_character * 7}|{logger.separating_character * 7}|{logger.separating_character * 10}|" 496 | logger.separator(breaker, space=False, border=False, 497 | side_space=False, left=True) 498 | for name, data in status.items(): 499 | logger.info( 500 | f"{name:<{longest}} | {data['added']:>5} | {data['unchanged']:>5} | {data['removed']:>5} | {data['run_time']:>8} | {data['status']}") 501 | if data["errors"]: 502 | for error in data["errors"]: 503 | logger.info(error) 504 | logger.info("") 505 | 506 | logger.info("") 507 | logger.separator("Summary") 508 | for library in config.libraries: 509 | logger.info("") 510 | logger.separator(f"{library.name} Summary", space=False, border=False) 511 | logger.info("") 512 | logger.info(f"{'Title':<27} | Run Time |") 513 | logger.info( 514 | f"{logger.separating_character * 27} | {logger.separating_character * 8} |") 515 | if library.name in library_status: 516 | for text, value in library_status[library.name].items(): 517 | logger.info(f"{text:<27} | {value:>8} |") 518 | logger.info("") 519 | print_status(library.status) 520 | if playlist_status: 521 | logger.info("") 522 | logger.separator(f"Playlists Summary", space=False, border=False) 523 | logger.info("") 524 | print_status(playlist_status) 525 | 526 | stats["added"] += amount_added 527 | for library in config.libraries: 528 | stats["created"] += library.stats["created"] 529 | stats["modified"] += library.stats["modified"] 530 | stats["deleted"] += library.stats["deleted"] 531 | stats["added"] += library.stats["added"] 532 | stats["unchanged"] += library.stats["unchanged"] 533 | stats["removed"] += library.stats["removed"] 534 | stats["radarr"] += library.stats["radarr"] 535 | stats["sonarr"] += library.stats["sonarr"] 536 | stats["names"].extend([{"name": n, "library": library.name} 537 | for n in library.stats["names"]]) 538 | if playlist_stats: 539 | stats["created"] += playlist_stats["created"] 540 | stats["modified"] += playlist_stats["modified"] 541 | stats["deleted"] += playlist_stats["deleted"] 542 | stats["added"] += playlist_stats["added"] 543 | stats["unchanged"] += playlist_stats["unchanged"] 544 | stats["removed"] += playlist_stats["removed"] 545 | stats["radarr"] += playlist_stats["radarr"] 546 | stats["sonarr"] += playlist_stats["sonarr"] 547 | stats["names"].extend([{"name": n, "library": "PLAYLIST"} 548 | for n in playlist_stats["names"]]) 549 | return stats 550 | 551 | 552 | def run_libraries(config): 553 | library_status = {} 554 | for library in config.libraries: 555 | if library.skip_library: 556 | logger.info("") 557 | logger.separator(f"Skipping {library.name} Library") 558 | continue 559 | library_status[library.name] = {} 560 | try: 561 | logger.add_library_handler(library.mapping_name) 562 | plexapi.server.TIMEOUT = library.timeout 563 | os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) 564 | logger.info("") 565 | logger.separator(f"{library.name} Library") 566 | 567 | logger.debug("") 568 | logger.debug(f"Mapping Name: {library.original_mapping_name}") 569 | logger.debug(f"Folder Name: {library.mapping_name}") 570 | for ad in library.asset_directory: 571 | logger.debug(f"Asset Directory: {ad}") 572 | logger.debug(f"Asset Folders: {library.asset_folders}") 573 | logger.debug( 574 | f"Create Asset Folders: {library.create_asset_folders}") 575 | logger.debug(f"Download URL Assets: {library.download_url_assets}") 576 | logger.debug(f"Sync Mode: {library.sync_mode}") 577 | logger.debug(f"Minimum Items: {library.minimum_items}") 578 | logger.debug( 579 | f"Delete Below Minimum: {library.delete_below_minimum}") 580 | logger.debug( 581 | f"Delete Not Scheduled: {library.delete_not_scheduled}") 582 | logger.debug( 583 | f"Default Collection Order: {library.default_collection_order}") 584 | logger.debug( 585 | f"Missing Only Released: {library.missing_only_released}") 586 | logger.debug(f"Only Filter Missing: {library.only_filter_missing}") 587 | logger.debug(f"Show Unmanaged: {library.show_unmanaged}") 588 | logger.debug(f"Show Filtered: {library.show_filtered}") 589 | logger.debug(f"Show Missing: {library.show_missing}") 590 | logger.debug(f"Show Missing Assets: {library.show_missing_assets}") 591 | logger.debug(f"Save Report: {library.save_report}") 592 | logger.debug(f"Report Path: {library.report_path}") 593 | logger.debug(f"Clean Bundles: {library.clean_bundles}") 594 | logger.debug(f"Empty Trash: {library.empty_trash}") 595 | logger.debug(f"Optimize: {library.optimize}") 596 | logger.debug(f"Timeout: {library.timeout}") 597 | 598 | if delete_collections and not playlist_only: 599 | time_start = datetime.now() 600 | logger.info("") 601 | logger.separator( 602 | f"Deleting all Collections from the {library.name} Library", space=False, border=False) 603 | logger.info("") 604 | for collection in library.get_all_collections(): 605 | try: 606 | library.delete(collection) 607 | logger.info(f"Collection {collection.title} Deleted") 608 | except Failed as e: 609 | logger.error(e) 610 | library_status[library.name]["All Collections Deleted"] = str( 611 | datetime.now() - time_start).split('.')[0] 612 | 613 | if delete_labels and not playlist_only: 614 | time_start = datetime.now() 615 | logger.info("") 616 | logger.separator( 617 | f"Deleting all Labels from All items in the {library.name} Library", space=False, border=False) 618 | logger.info("") 619 | if library.is_show: 620 | library_types = ["show", "season", "episode"] 621 | elif library.is_music: 622 | library_types = ["artist", "album", "track"] 623 | else: 624 | library_types = ["movie"] 625 | for library_type in library_types: 626 | for item in library.get_all(builder_level=library_type): 627 | try: 628 | library.edit_tags("label", item, sync_tags=[]) 629 | except NotFound: 630 | logger.error( 631 | f"{item.title[:25]:<25} | Labels Failed to be Removed") 632 | library_status[library.name]["All Labels Deleted"] = str( 633 | datetime.now() - time_start).split('.')[0] 634 | 635 | time_start = datetime.now() 636 | temp_items = None 637 | list_key = None 638 | expired = None 639 | if config.Cache: 640 | list_key, expired = config.Cache.query_list_cache( 641 | "library", library.mapping_name, 1) 642 | if cache_libraries and list_key and expired is False: 643 | logger.info( 644 | f"Library: {library.mapping_name} loaded from Cache") 645 | temp_items = config.Cache.query_list_ids(list_key) 646 | 647 | if not temp_items: 648 | temp_items = library.cache_items() 649 | if config.Cache and list_key: 650 | config.Cache.delete_list_ids(list_key) 651 | if config.Cache and cache_libraries: 652 | list_key = config.Cache.update_list_cache( 653 | "library", library.mapping_name, expired, 1) 654 | config.Cache.update_list_ids( 655 | list_key, [(i.ratingKey, i.guid) for i in temp_items]) 656 | if not library.is_music: 657 | logger.info("") 658 | logger.separator( 659 | f"Mapping {library.name} Library", space=False, border=False) 660 | logger.info("") 661 | library.map_guids(temp_items) 662 | library_status[library.name]["Library Loading and Mapping"] = str( 663 | datetime.now() - time_start).split('.')[0] 664 | 665 | def run_operations_and_overlays(): 666 | if not test and not collection_only and not playlist_only and not config.requested_metadata_files: 667 | if not overlays_only and library.library_operation: 668 | library_status[library.name]["Library Operations"] = library.Operations.run_operations( 669 | ) 670 | if not operations_only and (library.overlay_files or library.remove_overlays): 671 | library_status[library.name]["Library Overlays"] = library.Overlays.run_overlays( 672 | ) 673 | 674 | if library_first: 675 | run_operations_and_overlays() 676 | 677 | if not operations_only and not overlays_only and not playlist_only: 678 | time_start = datetime.now() 679 | for metadata in library.metadata_files: 680 | metadata_name = metadata.get_file_name() 681 | if config.requested_metadata_files and metadata_name not in config.requested_metadata_files: 682 | logger.info("") 683 | logger.separator( 684 | f"Skipping {metadata_name} Metadata File") 685 | continue 686 | logger.info("") 687 | logger.separator( 688 | f"Running {metadata_name} Metadata File\n{metadata.path}") 689 | if not test and not resume and not collection_only: 690 | try: 691 | metadata.update_metadata() 692 | except Failed as e: 693 | library.notify(e) 694 | logger.error(e) 695 | collections_to_run = metadata.get_collections( 696 | config.requested_collections) 697 | if resume and resume not in collections_to_run: 698 | logger.info("") 699 | logger.warning( 700 | f"Collection: {resume} not in Metadata File: {metadata.path}") 701 | continue 702 | if collections_to_run: 703 | logger.info("") 704 | logger.separator( 705 | f"{'Test ' if test else ''}Collections") 706 | logger.remove_library_handler(library.mapping_name) 707 | run_collection(config, library, metadata, 708 | collections_to_run) 709 | logger.re_add_library_handler(library.mapping_name) 710 | library_status[library.name]["Library Metadata Files"] = str( 711 | datetime.now() - time_start).split('.')[0] 712 | 713 | if not library_first: 714 | run_operations_and_overlays() 715 | 716 | logger.remove_library_handler(library.mapping_name) 717 | except Exception as e: 718 | library.notify(e) 719 | logger.stacktrace() 720 | logger.critical(e) 721 | return library_status 722 | 723 | 724 | def run_collection(config, library, metadata, requested_collections): 725 | global resume 726 | logger.info("") 727 | for mapping_name, collection_attrs in requested_collections.items(): 728 | collection_start = datetime.now() 729 | if test and ("test" not in collection_attrs or collection_attrs["test"] is not True): 730 | no_template_test = True 731 | if "template" in collection_attrs and collection_attrs["template"]: 732 | for data_template in util.get_list(collection_attrs["template"], split=False): 733 | if "name" in data_template \ 734 | and data_template["name"] \ 735 | and metadata.templates \ 736 | and data_template["name"] in metadata.templates \ 737 | and metadata.templates[data_template["name"]][0] \ 738 | and "test" in metadata.templates[data_template["name"]][0] \ 739 | and metadata.templates[data_template["name"]][0]["test"] is True: 740 | no_template_test = False 741 | if no_template_test: 742 | continue 743 | 744 | if resume and resume != mapping_name: 745 | continue 746 | elif resume == mapping_name: 747 | resume = None 748 | logger.info("") 749 | logger.separator(f"Resuming Collections") 750 | 751 | if "name_mapping" in collection_attrs and collection_attrs["name_mapping"]: 752 | collection_log_name, output_str = util.validate_filename( 753 | collection_attrs["name_mapping"]) 754 | else: 755 | collection_log_name, output_str = util.validate_filename( 756 | mapping_name) 757 | logger.add_collection_handler( 758 | library.mapping_name, collection_log_name) 759 | library.status[str(mapping_name)] = {"status": "Unchanged", "errors": [ 760 | ], "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0} 761 | 762 | try: 763 | builder = CollectionBuilder( 764 | config, metadata, mapping_name, collection_attrs, library=library, extra=output_str) 765 | library.stats["names"].append(builder.name) 766 | logger.info("") 767 | 768 | logger.separator( 769 | f"Running {mapping_name} Collection", space=False, border=False) 770 | 771 | if len(builder.schedule) > 0: 772 | logger.info(builder.schedule) 773 | 774 | if len(builder.smart_filter_details) > 0: 775 | logger.info("") 776 | logger.info(builder.smart_filter_details) 777 | logger.info("") 778 | logger.info(f"Items Found: {builder.beginning_count}") 779 | 780 | items_added = 0 781 | items_removed = 0 782 | if not builder.smart_url and builder.builders and not builder.blank_collection: 783 | logger.info("") 784 | logger.info( 785 | f"Sync Mode: {'sync' if builder.sync else 'append'}") 786 | 787 | for method, value in builder.builders: 788 | logger.debug("") 789 | logger.debug(f"Builder: {method}: {value}") 790 | logger.info("") 791 | try: 792 | builder.filter_and_save_items( 793 | builder.gather_ids(method, value)) 794 | except NonExisting as e: 795 | if builder.ignore_blank_results: 796 | logger.warning(e) 797 | else: 798 | raise Failed(e) 799 | 800 | if not builder.found_items and not builder.ignore_blank_results: 801 | raise NonExisting( 802 | f"{builder.Type} Warning: No items found") 803 | 804 | builder.display_filters() 805 | 806 | if len(builder.found_items) > 0 and len(builder.found_items) + builder.beginning_count >= builder.minimum and builder.build_collection: 807 | items_added, items_unchanged = builder.add_to_collection() 808 | library.stats["added"] += items_added 809 | library.status[str(mapping_name)]["added"] = items_added 810 | library.stats["unchanged"] += items_unchanged 811 | library.status[str(mapping_name) 812 | ]["unchanged"] = items_unchanged 813 | items_removed = 0 814 | if builder.sync: 815 | items_removed = builder.sync_collection() 816 | library.stats["removed"] += items_removed 817 | library.status[str(mapping_name) 818 | ]["removed"] = items_removed 819 | 820 | if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): 821 | radarr_add, sonarr_add = builder.run_missing() 822 | library.stats["radarr"] += radarr_add 823 | library.status[str(mapping_name)]["radarr"] += radarr_add 824 | library.stats["sonarr"] += sonarr_add 825 | library.status[str(mapping_name)]["sonarr"] += sonarr_add 826 | 827 | valid = True 828 | if builder.build_collection and not builder.blank_collection and items_added + builder.beginning_count < builder.minimum: 829 | logger.info("") 830 | logger.info( 831 | f"{builder.Type} Minimum: {builder.minimum} not met for {mapping_name} Collection") 832 | delete_status = f"Minimum {builder.minimum} Not Met" 833 | valid = False 834 | if builder.details["delete_below_minimum"] and builder.obj: 835 | logger.info("") 836 | logger.info(builder.delete()) 837 | library.stats["deleted"] += 1 838 | delete_status = f"Deleted; {delete_status}" 839 | library.status[str(mapping_name)]["status"] = delete_status 840 | 841 | run_item_details = True 842 | if valid and builder.build_collection and (builder.builders or builder.smart_url or builder.blank_collection): 843 | try: 844 | builder.load_collection() 845 | if builder.created: 846 | library.stats["created"] += 1 847 | library.status[str(mapping_name)]["status"] = "Created" 848 | elif items_added > 0 or items_removed > 0: 849 | library.stats["modified"] += 1 850 | library.status[str(mapping_name) 851 | ]["status"] = "Modified" 852 | except Failed: 853 | logger.stacktrace() 854 | run_item_details = False 855 | logger.info("") 856 | logger.separator( 857 | f"No {builder.Type} to Update", space=False, border=False) 858 | else: 859 | details_list = builder.update_details() 860 | if details_list: 861 | pre = "" 862 | if library.status[str(mapping_name)]["status"] != "Unchanged": 863 | pre = f"{library.status[str(mapping_name)]['status']} and " 864 | library.status[str( 865 | mapping_name)]["status"] = f"{pre}Updated {', '.join(details_list)}" 866 | 867 | if builder.server_preroll is not None: 868 | library.set_server_preroll(builder.server_preroll) 869 | logger.info("") 870 | logger.info( 871 | f"Plex Server Movie pre-roll video updated to {builder.server_preroll}") 872 | 873 | if valid and run_item_details and (builder.item_details or builder.custom_sort or builder.sync_to_trakt_list): 874 | try: 875 | builder.load_collection_items() 876 | except Failed: 877 | logger.info("") 878 | logger.separator("No Items Found", 879 | space=False, border=False) 880 | else: 881 | if builder.item_details: 882 | builder.update_item_details() 883 | if builder.custom_sort: 884 | builder.sort_collection() 885 | if builder.sync_to_trakt_list: 886 | builder.sync_trakt_list() 887 | 888 | builder.send_notifications() 889 | 890 | if builder.run_again and (len(builder.run_again_movies) > 0 or len(builder.run_again_shows) > 0): 891 | library.run_again.append(builder) 892 | 893 | except NonExisting as e: 894 | logger.warning(e) 895 | library.status[str(mapping_name)]["status"] = "Ignored" 896 | except NotScheduled as e: 897 | logger.info(e) 898 | if str(e).endswith("and was deleted"): 899 | library.notify_delete(e) 900 | library.stats["deleted"] += 1 901 | library.status[str(mapping_name) 902 | ]["status"] = "Deleted Not Scheduled" 903 | elif str(e).startswith("Skipped because allowed_library_types"): 904 | library.status[str(mapping_name) 905 | ]["status"] = "Skipped Invalid Library Type" 906 | else: 907 | library.status[str(mapping_name)]["status"] = "Not Scheduled" 908 | except FilterFailed: 909 | pass 910 | except Failed as e: 911 | library.notify(e, collection=mapping_name) 912 | logger.stacktrace() 913 | logger.error(e) 914 | library.status[str(mapping_name)]["status"] = "PMM Failure" 915 | library.status[str(mapping_name)]["errors"].append(e) 916 | except Exception as e: 917 | library.notify(f"Unknown Error: {e}", collection=mapping_name) 918 | logger.stacktrace() 919 | logger.error(f"Unknown Error: {e}") 920 | library.status[str(mapping_name)]["status"] = "Unknown Error" 921 | library.status[str(mapping_name)]["errors"].append(e) 922 | collection_run_time = str( 923 | datetime.now() - collection_start).split('.')[0] 924 | library.status[str(mapping_name)]["run_time"] = collection_run_time 925 | logger.info("") 926 | logger.separator( 927 | f"Finished {mapping_name} Collection\nCollection Run Time: {collection_run_time}") 928 | logger.remove_collection_handler( 929 | library.mapping_name, collection_log_name) 930 | 931 | 932 | def run_playlists(config): 933 | stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, 934 | "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0, "names": []} 935 | status = {} 936 | logger.info("") 937 | logger.separator("Playlists") 938 | logger.info("") 939 | for playlist_file in config.playlist_files: 940 | for mapping_name, playlist_attrs in playlist_file.playlists.items(): 941 | playlist_start = datetime.now() 942 | if test and ("test" not in playlist_attrs or playlist_attrs["test"] is not True): 943 | no_template_test = True 944 | if "template" in playlist_attrs and playlist_attrs["template"]: 945 | for data_template in util.get_list(playlist_attrs["template"], split=False): 946 | if "name" in data_template \ 947 | and data_template["name"] \ 948 | and playlist_file.templates \ 949 | and data_template["name"] in playlist_file.templates \ 950 | and playlist_file.templates[data_template["name"]][0] \ 951 | and "test" in playlist_file.templates[data_template["name"]][0] \ 952 | and playlist_file.templates[data_template["name"]][0]["test"] is True: 953 | no_template_test = False 954 | if no_template_test: 955 | continue 956 | 957 | if "name_mapping" in playlist_attrs and playlist_attrs["name_mapping"]: 958 | playlist_log_name, output_str = util.validate_filename( 959 | playlist_attrs["name_mapping"]) 960 | else: 961 | playlist_log_name, output_str = util.validate_filename( 962 | mapping_name) 963 | logger.add_playlist_handler(playlist_log_name) 964 | status[mapping_name] = {"status": "Unchanged", "errors": [ 965 | ], "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0} 966 | server_name = None 967 | library_names = None 968 | try: 969 | builder = CollectionBuilder( 970 | config, playlist_file, mapping_name, playlist_attrs, extra=output_str) 971 | stats["names"].append(builder.name) 972 | logger.info("") 973 | 974 | logger.separator( 975 | f"Running {mapping_name} Playlist", space=False, border=False) 976 | 977 | if len(builder.schedule) > 0: 978 | logger.info(builder.schedule) 979 | 980 | items_added = 0 981 | items_removed = 0 982 | valid = True 983 | logger.info("") 984 | logger.info( 985 | f"Sync Mode: {'sync' if builder.sync else 'append'}") 986 | 987 | method, value = builder.builders[0] 988 | logger.debug("") 989 | logger.debug(f"Builder: {method}: {value}") 990 | logger.info("") 991 | if method == "plex_watchlist": 992 | ids = builder.libraries[0].get_rating_keys( 993 | method, value, True) 994 | elif "plex" in method: 995 | ids = [] 996 | for pl_library in builder.libraries: 997 | try: 998 | ids.extend(pl_library.get_rating_keys( 999 | method, value, True)) 1000 | except Failed as e: 1001 | if builder.validate_builders: 1002 | raise 1003 | else: 1004 | logger.error(e) 1005 | elif "tautulli" in method: 1006 | ids = [] 1007 | for pl_library in builder.libraries: 1008 | try: 1009 | ids.extend( 1010 | pl_library.Tautulli.get_rating_keys(value, True)) 1011 | except Failed as e: 1012 | if builder.validate_builders: 1013 | raise 1014 | else: 1015 | logger.error(e) 1016 | else: 1017 | ids = builder.gather_ids(method, value) 1018 | 1019 | builder.display_filters() 1020 | builder.filter_and_save_items(ids) 1021 | 1022 | if len(builder.found_items) > 0 and len(builder.found_items) + builder.beginning_count >= builder.minimum: 1023 | items_added, items_unchanged = builder.add_to_collection() 1024 | stats["added"] += items_added 1025 | status[mapping_name]["added"] += items_added 1026 | stats["unchanged"] += items_unchanged 1027 | status[mapping_name]["unchanged"] += items_unchanged 1028 | items_removed = 0 1029 | if builder.sync: 1030 | items_removed = builder.sync_collection() 1031 | stats["removed"] += items_removed 1032 | status[mapping_name]["removed"] += items_removed 1033 | elif len(builder.found_items) < builder.minimum: 1034 | logger.info("") 1035 | logger.info( 1036 | f"Playlist Minimum: {builder.minimum} not met for {mapping_name} Playlist") 1037 | delete_status = f"Minimum {builder.minimum} Not Met" 1038 | valid = False 1039 | if builder.details["delete_below_minimum"] and builder.obj: 1040 | logger.info("") 1041 | logger.info(builder.delete()) 1042 | stats["deleted"] += 1 1043 | delete_status = f"Deleted; {delete_status}" 1044 | status[mapping_name]["status"] = delete_status 1045 | 1046 | if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): 1047 | radarr_add, sonarr_add = builder.run_missing() 1048 | stats["radarr"] += radarr_add 1049 | status[mapping_name]["radarr"] += radarr_add 1050 | stats["sonarr"] += sonarr_add 1051 | status[mapping_name]["sonarr"] += sonarr_add 1052 | 1053 | run_item_details = True 1054 | if valid and builder.builders: 1055 | try: 1056 | builder.load_collection() 1057 | if builder.created: 1058 | stats["created"] += 1 1059 | status[mapping_name]["status"] = "Created" 1060 | elif items_added > 0 or items_removed > 0: 1061 | stats["modified"] += 1 1062 | status[mapping_name]["status"] = "Modified" 1063 | except Failed: 1064 | logger.stacktrace() 1065 | run_item_details = False 1066 | logger.info("") 1067 | logger.separator("No Playlist to Update", 1068 | space=False, border=False) 1069 | else: 1070 | details_list = builder.update_details() 1071 | if details_list: 1072 | pre = "" 1073 | if status[mapping_name]["status"] != "Unchanged": 1074 | pre = f"{status[mapping_name]['status']} and " 1075 | status[mapping_name]["status"] = f"{pre}Updated {', '.join(details_list)}" 1076 | 1077 | if valid and run_item_details and builder.builders and (builder.item_details or builder.custom_sort): 1078 | try: 1079 | builder.load_collection_items() 1080 | except Failed: 1081 | logger.info("") 1082 | logger.separator("No Items Found", 1083 | space=False, border=False) 1084 | else: 1085 | if builder.item_details: 1086 | builder.update_item_details() 1087 | if builder.custom_sort: 1088 | builder.sort_collection() 1089 | 1090 | if valid: 1091 | builder.sync_playlist() 1092 | 1093 | builder.send_notifications(playlist=True) 1094 | 1095 | except Deleted as e: 1096 | logger.info(e) 1097 | status[mapping_name]["status"] = "Deleted" 1098 | config.notify_delete(e) 1099 | except NotScheduled as e: 1100 | logger.info(e) 1101 | if str(e).endswith("and was deleted"): 1102 | stats["deleted"] += 1 1103 | status[mapping_name]["status"] = "Deleted Not Scheduled" 1104 | config.notify_delete(e) 1105 | else: 1106 | status[mapping_name]["status"] = "Not Scheduled" 1107 | except Failed as e: 1108 | config.notify(e, server=server_name, 1109 | library=library_names, playlist=mapping_name) 1110 | logger.stacktrace() 1111 | logger.error(e) 1112 | status[mapping_name]["status"] = "PMM Failure" 1113 | status[mapping_name]["errors"].append(e) 1114 | except Exception as e: 1115 | config.notify( 1116 | f"Unknown Error: {e}", server=server_name, library=library_names, playlist=mapping_name) 1117 | logger.stacktrace() 1118 | logger.error(f"Unknown Error: {e}") 1119 | status[mapping_name]["status"] = "Unknown Error" 1120 | status[mapping_name]["errors"].append(e) 1121 | logger.info("") 1122 | playlist_run_time = str( 1123 | datetime.now() - playlist_start).split('.')[0] 1124 | status[mapping_name]["run_time"] = playlist_run_time 1125 | logger.info("") 1126 | logger.separator( 1127 | f"Finished {mapping_name} Playlist\nPlaylist Run Time: {playlist_run_time}") 1128 | logger.remove_playlist_handler(playlist_log_name) 1129 | return status, stats 1130 | 1131 | 1132 | if __name__ == "__main__": 1133 | try: 1134 | params = {"config_file": config_file, 1135 | "ignore_schedules": ignore_schedules} 1136 | if run or test or collections or libraries or metadata_files or resume: 1137 | params["collections"] = collections 1138 | params["libraries"] = libraries 1139 | params["metadata_files"] = metadata_files 1140 | process(params) 1141 | else: 1142 | times_to_run = util.get_list(times) 1143 | valid_times = [] 1144 | for time_to_run in times_to_run: 1145 | try: 1146 | valid_times.append(datetime.strftime( 1147 | datetime.strptime(time_to_run, "%H:%M"), "%H:%M")) 1148 | except ValueError: 1149 | if time_to_run: 1150 | raise Failed( 1151 | f"Argument Error: time argument invalid: {time_to_run} must be in the HH:MM format between 00:00-23:59") 1152 | else: 1153 | raise Failed(f"Argument Error: blank time argument") 1154 | for time_to_run in valid_times: 1155 | params["time"] = time_to_run 1156 | schedule.every().day.at(time_to_run).do(process, params) 1157 | while True: 1158 | schedule.run_pending() 1159 | if not no_countdown: 1160 | current_time = datetime.now().strftime("%H:%M") 1161 | seconds = None 1162 | og_time_str = "" 1163 | for time_to_run in valid_times: 1164 | new_seconds = (datetime.strptime( 1165 | time_to_run, "%H:%M") - datetime.strptime(current_time, "%H:%M")).total_seconds() 1166 | if new_seconds < 0: 1167 | new_seconds += 86400 1168 | if (seconds is None or new_seconds < seconds) and new_seconds > 0: 1169 | seconds = new_seconds 1170 | og_time_str = time_to_run 1171 | if seconds is not None: 1172 | hours = int(seconds // 3600) 1173 | minutes = int((seconds % 3600) // 60) 1174 | time_str = f"{hours} Hour{'s' if hours > 1 else ''} and " if hours > 0 else "" 1175 | time_str += f"{minutes} Minute{'s' if minutes > 1 else ''}" 1176 | logger.ghost( 1177 | f"Current Time: {current_time} | {time_str} until the next run at {og_time_str} | Runs: {', '.join(times_to_run)}") 1178 | else: 1179 | logger.error(f"Time Error: {valid_times}") 1180 | time.sleep(60) 1181 | except KeyboardInterrupt: 1182 | logger.separator("Exiting Plex Meta Manager") 1183 | --------------------------------------------------------------------------------