From 8574302f8dd299e1ef063ca1c9abe0cf0bba8397 Mon Sep 17 00:00:00 2001 From: Minimaxpower Date: Mon, 21 Sep 2026 05:07:49 +0200 Subject: [PATCH 1/2] Renomme le dossier sans espace, ajout recherche, filtre sans poster, mode sombre, upload manuel, badges poster/background --- .gitignore | 3 +- Plex Image Picker/app.py | 254 ------- Plex Image Picker/images/connect.png | Bin 37202 -> 0 bytes Plex Image Picker/images/episodes-posters.png | Bin 160951 -> 0 bytes Plex Image Picker/images/libraries.png | Bin 31601 -> 0 bytes .../images/movies-backgrounds.png | Bin 315105 -> 0 bytes Plex Image Picker/images/movies-download.png | Bin 398976 -> 0 bytes Plex Image Picker/images/movies-posters.png | Bin 385580 -> 0 bytes Plex Image Picker/images/seasons-posters.png | Bin 325440 -> 0 bytes Plex Image Picker/images/shows-posters.png | Bin 369936 -> 0 bytes Plex Image Picker/static/theme.js | 14 - Plex Image Picker/templates/item.html | 126 ---- Plex Image Picker/templates/items.html | 76 -- .../.envrc | 0 .../Dockerfile | 0 .../README.md | 0 plex-image-picker/app.py | 659 ++++++++++++++++++ .../requirements.txt | 0 plex-image-picker/static/theme.js | 19 + .../templates/base.html | 12 +- .../templates/connect.html | 6 +- plex-image-picker/templates/item.html | 201 ++++++ plex-image-picker/templates/items.html | 141 ++++ .../templates/libraries.html | 2 +- 24 files changed, 1034 insertions(+), 479 deletions(-) delete mode 100644 Plex Image Picker/app.py delete mode 100644 Plex Image Picker/images/connect.png delete mode 100644 Plex Image Picker/images/episodes-posters.png delete mode 100644 Plex Image Picker/images/libraries.png delete mode 100644 Plex Image Picker/images/movies-backgrounds.png delete mode 100644 Plex Image Picker/images/movies-download.png delete mode 100644 Plex Image Picker/images/movies-posters.png delete mode 100644 Plex Image Picker/images/seasons-posters.png delete mode 100644 Plex Image Picker/images/shows-posters.png delete mode 100644 Plex Image Picker/static/theme.js delete mode 100644 Plex Image Picker/templates/item.html delete mode 100644 Plex Image Picker/templates/items.html rename {Plex Image Picker => plex-image-picker}/.envrc (100%) rename {Plex Image Picker => plex-image-picker}/Dockerfile (100%) rename {Plex Image Picker => plex-image-picker}/README.md (100%) create mode 100644 plex-image-picker/app.py rename {Plex Image Picker => plex-image-picker}/requirements.txt (100%) create mode 100644 plex-image-picker/static/theme.js rename {Plex Image Picker => plex-image-picker}/templates/base.html (68%) rename {Plex Image Picker => plex-image-picker}/templates/connect.html (76%) create mode 100644 plex-image-picker/templates/item.html create mode 100644 plex-image-picker/templates/items.html rename {Plex Image Picker => plex-image-picker}/templates/libraries.html (64%) diff --git a/.gitignore b/.gitignore index 3b3739a..fe56f3b 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ Plex/assets* Other *-*-*-*-*.txt *-*-*-*-*-*.txt -*-*-*-*-*-*-*.txt \ No newline at end of file +*-*-*-*-*-*-*.txt +@eaDir/ diff --git a/Plex Image Picker/app.py b/Plex Image Picker/app.py deleted file mode 100644 index 4fd7c35..0000000 --- a/Plex Image Picker/app.py +++ /dev/null @@ -1,254 +0,0 @@ -import os -import math -import string -import requests -from flask import Flask, flash, redirect, render_template, request, session, url_for -from plexapi.server import PlexServer - -app = Flask(__name__) -app.secret_key = os.urandom(24) - -# module‑level alphabet list so we only build it once -ALPHABET = list(string.ascii_uppercase) -ALPHABET.insert(0, "0-9") - - -def get_plex(): - base_url = session.get("base_url") - token = session.get("token") - if not base_url or not token: - return None - return PlexServer(base_url, token) - - -@app.route("/", methods=["GET", "POST"]) -def connect(): - if request.method == "POST": - session["base_url"] = request.form["base_url"] - session["token"] = request.form["token"] - session["asset_dir"] = ( - "assets" if not request.form["asset_dir"] else request.form["asset_dir"] - ) - return redirect(url_for("libraries")) - return render_template("connect.html") - - -@app.route("/libraries") -def libraries(): - plex = get_plex() - if not plex: - return redirect(url_for("connect")) - sections = plex.library.sections() - return render_template("libraries.html", sections=sections) - - -# new paginated item picker -@app.route("/browse//items") -def list_items(section_key): - plex = get_plex() - if not plex: - return redirect(url_for("connect")) - - # find the right library section - section = next( - (s for s in plex.library.sections() if str(s.key) == section_key), - None, - ) - if not section: - flash("Library not found.") - return redirect(url_for("libraries")) - - # full list of items - all_items_full = list(section.all()) - - # give everything a global index - global_index = 1 - for item in all_items_full: - item.global_index = global_index - global_index = global_index + 1 - - # optional letter filter (defaults to 'All') - letter = request.args.get("letter", "All") - if letter != "All": - if letter[:1].isdigit(): - items_filtered = [ - item - for item in all_items_full - if item.title and item.title[:1].isdigit() - ] - else: - items_filtered = [ - item - for item in all_items_full - if item.title and item.title.upper().startswith(letter.upper()) - ] - else: - items_filtered = all_items_full - - # pagination params (based on filtered set) - total = len(items_filtered) - per_page = 20 - pages = math.ceil(total / per_page) - - # clamp page number - page = int(request.args.get("page", 1)) - page = max(1, min(page, pages)) - - # slice out this page from filtered items - start = (page - 1) * per_page - page_items = items_filtered[start : start + per_page] - - return render_template( - "items.html", - section=section, - items=page_items, - page=page, - pages=pages, - per_page=per_page, - alphabet=ALPHABET, - letter=letter, - ) - - -@app.route("/browse/") -def browse(section_key): - plex = get_plex() - if not plex: - return redirect(url_for("connect")) - section = next( - (s for s in plex.library.sections() if str(s.key) == section_key), None - ) - if not section: - flash("Library not found.") - return redirect(url_for("libraries")) - items = list(section.all()) - pages = len(items) - item_page = int(request.args.get("page", 1)) - item_letter = request.args.get("letter", "All") - item_page = max(1, min(item_page, pages)) - item = items[item_page - 1] - - art_type = request.args.get("art_type", "poster") - season = request.args.get("season") - try: - season_rating_key = item.season(int(season)).ratingKey - except: - season_rating_key = None - - episode = request.args.get("episode") - art_page = int(request.args.get("art_page", 1)) - - return render_template( - "item.html", - section=section, - item=item, - season_rating_key=season_rating_key, - items=items, - item_page=item_page, - item_letter=item_letter, - pages=pages, - art_type=art_type, - season=season, - episode=episode, - art_page=art_page, - ) - - -@app.route("/download", methods=["POST"]) -def download(): - plex = get_plex() - if not plex: - return redirect(url_for("connect")) - - rating_key = int(request.form["rating_key"]) - try: - season_rating_key = int(request.form["season_rating_key"]) - except: - season_rating_key = None - - # try: - # episode_rating_key = int(request.form['episode_rating_key']) - # except: - # episode_rating_key = None - section_key = request.form["section_key"] - art_type = request.form["art_type"] - img_key = request.form["img_key"] - season = ( - None if request.form.get("season") == "None" else request.form.get("season") - ) - episode = ( - None if request.form.get("episode") == "None" else request.form.get("episode") - ) - item_page = request.form.get("item_page", 1) - art_page = request.form.get("art_page", 1) - - item = plex.fetchItem(rating_key) - season_item = None if not season_rating_key else plex.fetchItem(season_rating_key) - # episode_item = None if not episode_rating_key else plex.fetchItem(episode_rating_key) - - try: - asset_name = None - if item.type == "movie": - media_file = item.media[0].parts[0].file - elif item.type == "show" or season_item: - media_file = item.locations[0] - asset_name = os.path.basename(media_file) - elif item.type == "episode": - media_file = item.media[0].parts[0].file - else: - media_file = None - if not asset_name: - asset_name = os.path.basename(os.path.dirname(media_file)) - except: - asset_name = item.title - - base_dir = os.path.join( - os.getcwd(), - session["asset_dir"], - "movies" if item.type == "movie" else "series", - asset_name, - ) - os.makedirs(base_dir, exist_ok=True) - - ext = os.path.splitext(img_key)[1] or ".jpg" - if item.type == "show": - if episode: - s, e = int(season), int(episode) - name = f"S{s:02d}E{e:02d}" - elif season: - s = int(season) - name = f"Season {s:02d}" - else: - name = art_type - suffix = ( - f"_{art_type}" if art_type == "background" and (season or episode) else "" - ) - filename = f"{name}{suffix}{ext}" - else: - filename = f"{art_type}{ext}" - - if img_key.startswith("http"): - img_url = img_key - else: - img_url = f"{session['base_url']}{img_key}&X-Plex-Token={session['token']}" - - resp = requests.get(img_url) - with open(os.path.join(base_dir, filename), "wb") as f: - f.write(resp.content) - - flash(f"Saved to {os.path.join(base_dir, filename)}") - return redirect( - url_for( - "browse", - section_key=section_key, - page=item_page, - art_type=art_type, - season=season, - episode=episode, - art_page=art_page, - ) - ) - - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=5000) diff --git a/Plex Image Picker/images/connect.png b/Plex Image Picker/images/connect.png deleted file mode 100644 index e513a66cfa43747a0210d5fc2495b24618720b7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37202 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