Skip to content

Commit 0ecde2d

Browse files
committed
Merge branch 'dev' of github.com:FuzzyGrim/Yamtrack into repeats_refactor
1 parent dde9cc7 commit 0ecde2d

13 files changed

+376
-98
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ You can try the app at [yamtrack.fuzzygrim.com](https://yamtrack.fuzzygrim.com)
2525
- 🐳 Easy deployment with Docker via docker-compose with SQLite or PostgreSQL.
2626
- 👥 Multi-users functionality allowing individual accounts with personalized tracking.
2727
- 🔑 Flexible authentication options including OIDC and 100+ social providers (Google, GitHub, Discord, etc.) via django-allauth.
28-
- 🦀 Integration with [Jellyfin](https://jellyfin.org/), to automatically track new media watched.
28+
- 🦀 Integration with [Jellyfin](https://jellyfin.org/) and [Plex](https://plex.tv), to automatically track new media watched.
2929
- 📥 Import from [Trakt](https://trakt.tv/), [Simkl](https://simkl.com/), [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/) and [Kitsu](https://kitsu.app/) with support for periodic automatic imports.
3030
- 📊 Export all your tracked media to a CSV file and import it back.
3131

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from django.db import migrations
2+
from django.conf import settings
3+
from app.models import MediaTypes
4+
from app.providers import services
5+
import logging
6+
7+
logger = logging.getLogger(__name__)
8+
9+
def update_episode_images(apps, schema_editor):
10+
Item = apps.get_model('app', 'Item')
11+
12+
# Get all episode items with default image
13+
episode_items = Item.objects.filter(
14+
media_type=MediaTypes.EPISODE.value,
15+
image=settings.IMG_NONE
16+
)
17+
18+
if not episode_items.exists():
19+
return
20+
21+
logger.info("Starting episode image update migration")
22+
logger.info("Found %s episodes with default images to process", episode_items.count())
23+
24+
items_to_update = []
25+
26+
for item in episode_items:
27+
try:
28+
29+
logger.info(
30+
"Updating image for %s S%sE%s",
31+
item.title,
32+
item.season_number,
33+
item.episode_number
34+
)
35+
season_metadata = services.get_media_metadata(
36+
MediaTypes.SEASON.value,
37+
item.media_id,
38+
item.source,
39+
[item.season_number]
40+
)
41+
42+
for ep_meta in season_metadata.get('episodes', []):
43+
if ep_meta['episode_number'] == int(item.episode_number):
44+
if ep_meta.get('still_path'):
45+
item.image = f"https://image.tmdb.org/t/p/original{ep_meta['still_path']}"
46+
elif 'image' in ep_meta:
47+
item.image = ep_meta['image']
48+
items_to_update.append(item)
49+
break
50+
51+
except Exception as e:
52+
print(f"Failed to update image for episode {item.id}: {str(e)}")
53+
54+
if items_to_update:
55+
Item.objects.bulk_update(items_to_update, ['image'])
56+
57+
class Migration(migrations.Migration):
58+
dependencies = [
59+
('app', '0043_remove_historicalanime_progress_changed_and_more'),
60+
]
61+
62+
operations = [
63+
migrations.RunPython(update_episode_images, reverse_code=migrations.RunPython.noop),
64+
]

src/app/migrations/0044_alter_episode_options_and_more.py renamed to src/app/migrations/0045_alter_episode_options_and_more.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def convert_repeats_to_instances(apps, schema_editor):
2929
class Migration(migrations.Migration):
3030

3131
dependencies = [
32-
('app', '0043_remove_historicalanime_progress_changed_and_more'),
32+
('app', '0044_fix_episode_images'),
3333
]
3434

3535
operations = [

src/app/migrations/0045_alter_anime_options_alter_basicmedia_options_and_more.py renamed to src/app/migrations/0046_alter_anime_options_alter_basicmedia_options_and_more.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def convert_media_repeats_to_instances(apps, schema_editor):
8383
class Migration(migrations.Migration):
8484

8585
dependencies = [
86-
('app', '0044_alter_episode_options_and_more'),
86+
('app', '0045_alter_episode_options_and_more'),
8787
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
8888
]
8989

src/app/migrations/0046_remove_anime_repeats_remove_basicmedia_repeats_and_more.py renamed to src/app/migrations/0047_remove_anime_repeats_remove_basicmedia_repeats_and_more.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
class Migration(migrations.Migration):
77

88
dependencies = [
9-
('app', '0045_alter_anime_options_alter_basicmedia_options_and_more'),
9+
('app', '0046_alter_anime_options_alter_basicmedia_options_and_more'),
1010
]
1111

1212
operations = [

src/app/migrations/0047_alter_anime_options_alter_basicmedia_options_and_more.py renamed to src/app/migrations/0048_alter_anime_options_alter_basicmedia_options_and_more.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
class Migration(migrations.Migration):
77

88
dependencies = [
9-
('app', '0046_remove_anime_repeats_remove_basicmedia_repeats_and_more'),
9+
('app', '0047_remove_anime_repeats_remove_basicmedia_repeats_and_more'),
1010
]
1111

1212
operations = [

src/app/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,13 @@ def process_status(self):
795795
if max_progress:
796796
self.progress = max_progress
797797

798+
previous_repeating = (
799+
self.tracker.previous("status") == self.Status.REPEATING.value
800+
)
801+
repeat_count_not_updated = self.repeats == self.tracker.previous("repeats")
802+
if previous_repeating and repeat_count_not_updated:
803+
self.repeats += 1
804+
798805
self.item.fetch_releases(delay=True)
799806

800807
@property

src/app/providers/tmdb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def find(external_id, external_source):
118118

119119
cache.set(cache_key, data)
120120

121-
return response["tv_episode_results"][0] if response["tv_episode_results"] else None
121+
return response
122122

123123

124124
def movie(media_id):

src/app/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,8 +512,8 @@ def media_delete(request):
512512
def episode_save(request):
513513
"""Handle the creation, deletion, and updating of episodes for a season."""
514514
media_id = request.POST["media_id"]
515-
season_number = request.POST["season_number"]
516-
episode_number = request.POST["episode_number"]
515+
season_number = int(request.POST["season_number"])
516+
episode_number = int(request.POST["episode_number"])
517517
source = request.POST["source"]
518518

519519
try:

src/config/settings.py

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,60 @@
77
from urllib.parse import urlparse
88

99
from celery.schedules import crontab
10-
from decouple import Csv, config
10+
from decouple import (
11+
Config,
12+
Csv,
13+
RepositorySecret,
14+
Undefined,
15+
UndefinedValueError,
16+
config,
17+
undefined,
18+
)
1119
from django.core.cache import CacheKeyWarning
1220

1321
# Build paths inside the project like this: BASE_DIR / 'subdir'.
1422
BASE_DIR = Path(__file__).resolve().parent.parent
1523

1624

25+
def secret(key, default=undefined, **kwargs):
26+
"""Try to read a config value from a secret file.
27+
28+
If only the filename is given, try to read from /run/secrets/<key>.
29+
If an absolute path is specified, try to read from this path.
30+
"""
31+
if isinstance(default, Undefined):
32+
default = None
33+
34+
file = config(key, default, **kwargs)
35+
36+
if file is None:
37+
return undefined
38+
if file == default:
39+
return default
40+
41+
path = Path(file)
42+
try:
43+
if path.is_absolute():
44+
return Config(RepositorySecret(path.parent))(path.stem, default, **kwargs)
45+
return Config(RepositorySecret())(file, default, **kwargs)
46+
except (
47+
FileNotFoundError,
48+
IsADirectoryError,
49+
UndefinedValueError,
50+
) as err:
51+
msg = f"File from {key} not found. Please check the path and filename."
52+
raise UndefinedValueError(msg) from err
53+
54+
1755
# Quick-start development settings - unsuitable for production
1856
# See https://docs.djangoproject.com/en/stable/howto/deployment/checklist/
1957

2058
# SECURITY WARNING: keep the secret key used in production secret!
21-
SECRET_KEY = config("SECRET", default="secret")
59+
SECRET_KEY = config(
60+
"SECRET",
61+
default=secret("SECRET_FILE", default="ifx7bdUWo5EwC2NQNihjRjOrW00Cdv5Y"),
62+
)
63+
2264

2365
# SECURITY WARNING: don't run with debug turned on in production!
2466
DEBUG = config("DEBUG", default=False, cast=bool)
@@ -132,9 +174,9 @@
132174
"default": {
133175
"ENGINE": "django.db.backends.postgresql",
134176
"HOST": config("DB_HOST"),
135-
"NAME": config("DB_NAME"),
136-
"USER": config("DB_USER"),
137-
"PASSWORD": config("DB_PASSWORD"),
177+
"NAME": config("DB_NAME", default=secret("DB_NAME_FILE")),
178+
"USER": config("DB_USER", default=secret("DB_USER_FILE")),
179+
"PASSWORD": config("DB_PASSWORD", default=secret("DB_PASSWORD_FILE")),
138180
"PORT": config("DB_PORT"),
139181
},
140182
}
@@ -246,7 +288,7 @@
246288
# Yamtrack settings
247289

248290
# For CSV imports
249-
FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB
291+
FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB
250292

251293
VERSION = config("VERSION", default="dev")
252294

@@ -259,41 +301,86 @@
259301
REQUEST_TIMEOUT = 120 # seconds
260302
PER_PAGE = 24
261303

262-
TMDB_API = config("TMDB_API", default="61572be02f0a068658828f6396aacf60")
304+
TMDB_API = config(
305+
"TMDB_API",
306+
default=secret(
307+
"TMDB_API_FILE",
308+
"61572be02f0a068658828f6396aacf60",
309+
),
310+
)
263311
TMDB_NSFW = config("TMDB_NSFW", default=False, cast=bool)
264312
TMDB_LANG = config("TMDB_LANG", default="en")
265313

266-
MAL_API = config("MAL_API", default="25b5581dafd15b3e7d583bb79e9a1691")
314+
MAL_API = config(
315+
"MAL_API",
316+
default=secret(
317+
"MAL_API_FILE",
318+
"25b5581dafd15b3e7d583bb79e9a1691",
319+
),
320+
)
267321
MAL_NSFW = config("MAL_NSFW", default=False, cast=bool)
268322

269323
MU_NSFW = config("MU_NSFW", default=False, cast=bool)
270324

271-
IGDB_ID = config("IGDB_ID", default="8wqmm7x1n2xxtnz94lb8mthadhtgrt")
272-
IGDB_SECRET = config("IGDB_SECRET", default="ovbq0hwscv58hu46yxn50hovt4j8kj")
325+
IGDB_ID = config(
326+
"IGDB_ID",
327+
default=secret(
328+
"IGDB_ID_FILE",
329+
"8wqmm7x1n2xxtnz94lb8mthadhtgrt",
330+
),
331+
)
332+
IGDB_SECRET = config(
333+
"IGDB_SECRET",
334+
default=secret(
335+
"IGDB_SECRET_FILE",
336+
"ovbq0hwscv58hu46yxn50hovt4j8kj",
337+
),
338+
)
273339
IGDB_NSFW = config("IGDB_NSFW", default=False, cast=bool)
274340

275341
HARDCOVER_API = config(
276342
"HARDCOVER_API",
277-
default="Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJIYXJkY292ZXIiLCJ2ZXJzaW9uIjoiOCIsImp0aSI6ImJhNGNjZmUwLTgwZmQtNGI3NC1hZDdhLTlkNDM5ZTA5YWMzOSIsImFwcGxpY2F0aW9uSWQiOjIsInN1YiI6IjM0OTUxIiwiYXVkIjoiMSIsImlkIjoiMzQ5NTEiLCJsb2dnZWRJbiI6dHJ1ZSwiaWF0IjoxNzQ2OTc3ODc3LCJleHAiOjE3Nzg1MTM4NzcsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtYWxsb3dlZC1yb2xlcyI6WyJ1c2VyIl0sIngtaGFzdXJhLWRlZmF1bHQtcm9sZSI6InVzZXIiLCJ4LWhhc3VyYS1yb2xlIjoidXNlciIsIlgtaGFzdXJhLXVzZXItaWQiOiIzNDk1MSJ9LCJ1c2VyIjp7ImlkIjozNDk1MX19.edcEqLAeO3uH5xxBTFDKtyWwi-B-WfXX_yiLFdOAJ3c", # noqa: E501
343+
default=secret(
344+
"HARDCOVER_API_FILE",
345+
"Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJIYXJkY292ZXIiLCJ2ZXJzaW9uIjoiOCIsImp0"
346+
"aSI6ImJhNGNjZmUwLTgwZmQtNGI3NC1hZDdhLTlkNDM5ZTA5YWMzOSIsImFwcGxpY2F0aW9uSWQi"
347+
"OjIsInN1YiI6IjM0OTUxIiwiYXVkIjoiMSIsImlkIjoiMzQ5NTEiLCJsb2dnZWRJbiI6dHJ1ZSwi"
348+
"aWF0IjoxNzQ2OTc3ODc3LCJleHAiOjE3Nzg1MTM4NzcsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9j"
349+
"bGFpbXMiOnsieC1oYXN1cmEtYWxsb3dlZC1yb2xlcyI6WyJ1c2VyIl0sIngtaGFzdXJhLWRlZmF1"
350+
"bHQtcm9sZSI6InVzZXIiLCJ4LWhhc3VyYS1yb2xlIjoidXNlciIsIlgtaGFzdXJhLXVzZXItaWQi"
351+
"OiIzNDk1MSJ9LCJ1c2VyIjp7ImlkIjozNDk1MX19.edcEqLAeO3uH5xxBTFDKtyWwi-B-WfXX_yi"
352+
"LFdOAJ3c",
353+
),
278354
)
279355

280356
COMICVINE_API = config(
281357
"COMICVINE_API",
282-
default="cdab0706269e4bca03a096fbc39920dadf7e4992",
358+
default=secret(
359+
"COMICVINE_API_FILE",
360+
"cdab0706269e4bca03a096fbc39920dadf7e4992",
361+
),
283362
)
284363

285-
286364
TRAKT_API = config(
287365
"TRAKT_API",
288-
default="b4d9702b11cfaddf5e863001f68ce9d4394b678926e8a3f64d47bf69a55dd0fe",
366+
default=secret(
367+
"TRAKT_API_FILE",
368+
"b4d9702b11cfaddf5e863001f68ce9d4394b678926e8a3f64d47bf69a55dd0fe",
369+
),
289370
)
290371
SIMKL_ID = config(
291372
"SIMKL_ID",
292-
default="f1df351ddbace7e2c52f0010efdeb1fd59d379d9cdfb88e9a847c68af410db0e",
373+
default=secret(
374+
"SIMKL_ID_FILE",
375+
"f1df351ddbace7e2c52f0010efdeb1fd59d379d9cdfb88e9a847c68af410db0e",
376+
),
293377
)
294378
SIMKL_SECRET = config(
295379
"SIMKL_SECRET",
296-
default="9bb254894a598894bee14f61eafdcdca47622ab346632f951ed7220a3de289b5",
380+
default=secret(
381+
"SIMKL_SECRET_FILE",
382+
"9bb254894a598894bee14f61eafdcdca47622ab346632f951ed7220a3de289b5",
383+
),
297384
)
298385

299386
TESTING = False
@@ -407,7 +494,10 @@
407494

408495
SOCIALACCOUNT_PROVIDERS = config(
409496
"SOCIALACCOUNT_PROVIDERS",
410-
default="{}",
497+
default=secret(
498+
"SOCIALACCOUNT_PROVIDERS_FILE",
499+
default="{}",
500+
),
411501
cast=json.loads,
412502
)
413503

0 commit comments

Comments
 (0)