Skip to content

Commit 91ffe74

Browse files
committed
new image picker app
1 parent a39f511 commit 91ffe74

File tree

12 files changed

+504
-395
lines changed

12 files changed

+504
-395
lines changed

Plex Image Picker/.envrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
layout pyenv 3.12.6
2+
python -m pip install --upgrade pip
3+
python -m pip install -r requirements.txt
4+

Plex Image Picker/Dockerfile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
FROM python:3.9-slim
2+
ENV TINI_VERSION=v0.19.0
3+
4+
WORKDIR /
5+
COPY requirements.txt requirements.txt
6+
7+
RUN echo "**** install system packages ****" \
8+
&& apt-get update \
9+
&& apt-get upgrade -y --no-install-recommends \
10+
&& apt-get install -y tzdata --no-install-recommends \
11+
&& apt-get install -y wget curl nano \
12+
&& wget -O /tini https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-"$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
13+
&& chmod +x /tini \
14+
&& pip3 install --no-cache-dir --upgrade --requirement /requirements.txt \
15+
&& apt-get clean \
16+
&& apt-get update \
17+
&& apt-get check \
18+
&& apt-get -f install \
19+
&& apt-get autoclean \
20+
&& rm -rf /requirements.txt /tmp/* /var/tmp/* /var/lib/apt/lists/*
21+
COPY . /
22+
23+
VOLUME /assets
24+
25+
EXPOSE 5000
26+
ENV FLASK_APP=app.py
27+
ENTRYPOINT ["/tini", "--"]
28+
29+
CMD ["flask", "run", "--host=0.0.0.0"]
30+

Plex Image Picker/README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Plex Image Picker
2+
3+
You want a simple way to choose which Plex-supplied image you want in your asset directory.
4+
5+
This presents a web UI that lets you scroll through the images that plex provides for each item [movie, show, season, episode], selecting the one you want by clicking a button.
6+
7+
When you click on an image, it is downloaded to a file system rooted at `assets` with the correct pathing and naming for the Kometa asset directory.
8+
9+
You can then copy that `assets` directory to the Kometa config dir ready for use.
10+
11+
This script does not use anything from the `.env`, but it does make some assumptions:
12+
13+
## Requirements
14+
15+
1. A system that can run Python 3.7 [or newer]
16+
1. Python 3.7 [or newer] installed on that system
17+
18+
One of the requirements of these scripts is alive-progress 2.4.1, which requires Python 3.7.
19+
1. A basic knowledge of how to run Python scripts.
20+
1. You can run a web server on the machine that listens on port 5000
21+
22+
## Setup
23+
24+
### if you use [`direnv`](https://github.com/direnv/direnv) and [`pyenv`](https://github.com/pyenv/pyenv):
25+
1. clone the repo
26+
1. cd into the repo dir
27+
1. cd into the app directory [`cd "Plex Image Picker"`]
28+
1. run `direnv allow` as the prompt will tell you to
29+
1. direnv will build the virtual env and keep requirements up to date
30+
31+
### if you don't use [`direnv`](https://github.com/direnv/direnv) and [`pyenv`](https://github.com/pyenv/pyenv):
32+
1. install direnv and pyenv
33+
2. go to the previous section
34+
35+
Ha ha only serious.
36+
37+
To set it up without those things:
38+
39+
#### Python
40+
41+
1. clone repo
42+
```
43+
git clone https://github.com/chazlarson/Media-Scripts.git
44+
```
45+
1. cd to repo directory
46+
```
47+
cd Media-Scripts
48+
```
49+
1. cd to application directory
50+
```
51+
cd Plex Image Picker
52+
```
53+
1. Install requirements with `python3 -m pip install -r requirements.txt` [I'd suggest doing this in a virtual environment]
54+
55+
Creating a virtual environment is described [here](https://docs.python.org/3/library/venv.html); there's also a step-by-step in the local walkthrough in the Kometa wiki.
56+
1. Run with `flask run`
57+
1. Go to one of the URLs presented.
58+
59+
#### Docker
60+
61+
1. build the docker image:
62+
```
63+
docker build -t plex-image-picker .
64+
```
65+
2. run the container:
66+
```
67+
docker run -p 5000:5000 -v /opt/Media-Scripts/plex_art_app/assets:/assets:rw plex-image-picker
68+
```
69+
Change the port and path as needed, of course.
70+
3. Go to one of the URLs presented, as appropriate.
71+
72+
#### Interface
73+
74+
You'll be asked to enter connection details. You can change the directory where the images will go on this initial setup screen:
75+
76+
![](images/connect.png)
77+
78+
Then you will choose a library:
79+
80+
![](images/libraries.png)
81+
82+
You'll see a grid displaying all posters available for the first item in the library:
83+
84+
![](images/movies-posters.png)
85+
86+
You can choose to view backgrounds instead of posters:
87+
88+
![](images/movies-backgrounds.png)
89+
90+
When you click the Download button, the file will be downloaded to the asset directory; a flash message will report the path:
91+
92+
![](images/movies-download.png)
93+
94+
Click through the movies/shows with the top row of buttons; click through pages of images with the lower set of buttons.
95+
96+
Navigate to a specific page of posters with the dropdown on the right.
97+
98+
Clicking the title in the upper left will take you back to the library list.
99+
100+
In a Show library, you will be able to choose to view posters/backgrounds for shows, seasons, or episodes:
101+
102+
![](images/shows-posters.png)
103+
![](images/seasons-posters.png)
104+
![](images/episodes-posters.png)

Plex Image Picker/app.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from flask import Flask, render_template, request, redirect, url_for, session, flash
2+
from plexapi.server import PlexServer
3+
import os, requests
4+
5+
app = Flask(__name__)
6+
app.secret_key = os.urandom(24)
7+
8+
def get_plex():
9+
base_url = session.get('base_url')
10+
token = session.get('token')
11+
if not base_url or not token:
12+
return None
13+
return PlexServer(base_url, token)
14+
15+
@app.route('/', methods=['GET', 'POST'])
16+
def connect():
17+
if request.method == 'POST':
18+
session['base_url'] = request.form['base_url']
19+
session['token'] = request.form['token']
20+
session['asset_dir'] = 'assets' if not request.form['asset_dir'] else request.form['asset_dir']
21+
return redirect(url_for('libraries'))
22+
return render_template('connect.html')
23+
24+
@app.route('/libraries')
25+
def libraries():
26+
plex = get_plex()
27+
if not plex:
28+
return redirect(url_for('connect'))
29+
sections = plex.library.sections()
30+
return render_template('libraries.html', sections=sections)
31+
32+
# http://127.0.0.1:5000/browse/5?page=1&art_type=poster&art_page=1&season=3&episode=
33+
34+
@app.route('/browse/<section_key>')
35+
def browse(section_key):
36+
plex = get_plex()
37+
if not plex:
38+
return redirect(url_for('connect'))
39+
section = next((s for s in plex.library.sections() if str(s.key) == section_key), None)
40+
if not section:
41+
flash('Library not found.')
42+
return redirect(url_for('libraries'))
43+
items = list(section.all())
44+
pages = len(items)
45+
item_page = int(request.args.get('page', 1))
46+
item_page = max(1, min(item_page, pages))
47+
item = items[item_page - 1]
48+
49+
art_type = request.args.get('art_type', 'poster')
50+
season = request.args.get('season')
51+
try:
52+
season_rating_key = item.season(int(season)).ratingKey
53+
except:
54+
season_rating_key = None
55+
56+
episode = request.args.get('episode')
57+
art_page = int(request.args.get('art_page', 1))
58+
59+
return render_template('item.html',
60+
section=section,
61+
item=item,
62+
season_rating_key=season_rating_key,
63+
items=items,
64+
item_page=item_page,
65+
pages=pages,
66+
art_type=art_type,
67+
season=season,
68+
episode=episode,
69+
art_page=art_page)
70+
71+
@app.route('/download', methods=['POST'])
72+
def download():
73+
plex = get_plex()
74+
if not plex:
75+
return redirect(url_for('connect'))
76+
77+
rating_key = int(request.form['rating_key'])
78+
try:
79+
season_rating_key = int(request.form['season_rating_key'])
80+
except:
81+
season_rating_key = None
82+
83+
try:
84+
episode_rating_key = int(request.form['episode_rating_key'])
85+
except:
86+
episode_rating_key = None
87+
section_key = request.form['section_key']
88+
art_type = request.form['art_type']
89+
img_key = request.form['img_key']
90+
season = None if request.form.get('season') == 'None' else request.form.get('season')
91+
episode = None if request.form.get('episode') == 'None' else request.form.get('episode')
92+
item_page = request.form.get('item_page', 1)
93+
art_page = request.form.get('art_page', 1)
94+
95+
item = plex.fetchItem(rating_key)
96+
season_item = None if not season_rating_key else plex.fetchItem(season_rating_key)
97+
episode_item = None if not episode_rating_key else plex.fetchItem(episode_rating_key)
98+
99+
try:
100+
asset_name = None
101+
if item.type == 'movie':
102+
media_file = item.media[0].parts[0].file
103+
elif item.type == 'show' or season_item:
104+
media_file = item.locations[0]
105+
asset_name = os.path.basename(media_file)
106+
elif item.type == 'episode':
107+
media_file = item.media[0].parts[0].file
108+
else:
109+
media_file = None
110+
if not asset_name:
111+
asset_name = os.path.basename(os.path.dirname(media_file))
112+
except:
113+
asset_name = item.title
114+
115+
base_dir = os.path.join(os.getcwd(), session['asset_dir'],
116+
'movies' if item.type == 'movie' else 'series',
117+
asset_name)
118+
os.makedirs(base_dir, exist_ok=True)
119+
120+
ext = os.path.splitext(img_key)[1] or '.jpg'
121+
if item.type == 'show':
122+
if episode:
123+
s, e = int(season), int(episode)
124+
name = f"S{s:02d}E{e:02d}"
125+
elif season:
126+
s = int(season)
127+
name = f"Season {s:02d}"
128+
else:
129+
name = art_type
130+
suffix = f"_{art_type}" if art_type == 'background' and (season or episode) else ''
131+
filename = f"{name}{suffix}{ext}"
132+
else:
133+
filename = f"{art_type}{ext}"
134+
135+
if img_key.startswith("http"):
136+
img_url = img_key
137+
else:
138+
img_url = f"{session['base_url']}{img_key}&X-Plex-Token={session['token']}"
139+
140+
resp = requests.get(img_url)
141+
with open(os.path.join(base_dir, filename), 'wb') as f:
142+
f.write(resp.content)
143+
144+
flash(f"Saved to {os.path.join(base_dir, filename)}")
145+
return redirect(url_for('browse',
146+
section_key=section_key,
147+
page=item_page,
148+
art_type=art_type,
149+
season=season,
150+
episode=episode,
151+
art_page=art_page))
152+
153+
if __name__ == '__main__':
154+
app.run(host='0.0.0.0', port=5000)

Plex Image Picker/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Flask
2+
plexapi
3+
requests

Plex Image Picker/static/theme.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
(function() {
2+
const selector = document.getElementById('theme-selector');
3+
function applyTheme(theme) {
4+
document.documentElement.setAttribute('data-theme', theme);
5+
}
6+
selector.addEventListener('change', () => applyTheme(selector.value));
7+
if (selector.value === 'auto') {
8+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
9+
mq.addEventListener('change', e => applyTheme(mq.matches ? 'dark' : 'light'));
10+
applyTheme(mq.matches ? 'dark' : 'light');
11+
} else {
12+
applyTheme(selector.value);
13+
}
14+
})();
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!doctype html>
2+
<html lang="en" data-theme="auto">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Plex Art Downloader</title>
7+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8+
<script src="{{ url_for('static', filename='theme.js') }}"></script>
9+
</head>
10+
<body class="bg-light text-dark">
11+
<nav class="navbar navbar-expand-lg navbar-light bg-white mb-4">
12+
<div class="container-fluid">
13+
<a class="navbar-brand" href="{{ url_for('libraries') }}">Plex Image Picker</a>
14+
</div>
15+
</nav>
16+
<div class="container">
17+
{% with msgs = get_flashed_messages() %}
18+
{% if msgs %}
19+
<div class="alert alert-info">
20+
{% for m in msgs %} <p>{{ m }}</p> {% endfor %}
21+
</div>
22+
{% endif %}
23+
{% endwith %}
24+
{% block content %}{% endblock %}
25+
</div>
26+
</body>
27+
</html>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% extends 'base.html' %}
2+
{% block content %}
3+
<h2>Connect to Plex</h2>
4+
<form method="post">
5+
<div class="mb-3">
6+
<label class="form-label">Base URL</label>
7+
<input type="text" name="base_url" class="form-control" placeholder="http://localhost:32400" required>
8+
</div>
9+
<div class="mb-3">
10+
<label class="form-label">Token</label>
11+
<input type="text" name="token" class="form-control" placeholder="YOUR_PLEX_TOKEN" required>
12+
</div>
13+
<div class="mb-3">
14+
<label class="form-label">Base Image Asset directory</label>
15+
<input type="text" name="asset_dir" class="form-control" placeholder="assets">
16+
</div>
17+
<button type="submit" class="btn btn-primary">Connect</button>
18+
</form>
19+
{% endblock %}

0 commit comments

Comments
 (0)