mirror of
https://github.com/FuzzyGrim/Yamtrack.git
synced 2026-06-28 06:45:58 +00:00
refactor: better oauth handling for trakt and simkl
Get username/user ID from service and store in periodic task to show in UI instead of encrypted token Remove old Trakt importer, make new oauth importer backwards-compatible Add migration for SIMKL periodic tasks to new username+token format
This commit is contained in:
29
src/app/migrations/0051_migrate_simkl_periodoc_tasks.py
Normal file
29
src/app/migrations/0051_migrate_simkl_periodoc_tasks.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.2.2 on 2025-07-29 13:15
|
||||
|
||||
from django.db import migrations
|
||||
from integrations.imports import helpers, simkl
|
||||
import json
|
||||
|
||||
def migrate_simkl_tasks(apps, schema_editor):
|
||||
PeriodicTask = apps.get_model('django_celery_beat', 'PeriodicTask')
|
||||
tasks = PeriodicTask.objects.filter(name__startswith='Import from SIMKL')
|
||||
for task in tasks:
|
||||
# Update the task name to match the new import function
|
||||
kwargs = json.loads(task.kwargs)
|
||||
if hasattr(kwargs, "token"):
|
||||
continue
|
||||
kwargs["token"]= kwargs["username"]
|
||||
kwargs["username"]=simkl.get_username(helpers.decrypt(kwargs["token"]))
|
||||
task.name = task.name.replace(kwargs["token"], kwargs["username"])
|
||||
task.kwargs = json.dumps(kwargs)
|
||||
task.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0050_fix_null_history_users'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_simkl_tasks, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -190,7 +190,15 @@ def bulk_create_media(bulk_media_list, user):
|
||||
)
|
||||
|
||||
|
||||
def create_import_schedule(username, request, mode, frequency, import_time, source):
|
||||
def create_import_schedule(
|
||||
username,
|
||||
request,
|
||||
mode,
|
||||
frequency,
|
||||
import_time,
|
||||
source,
|
||||
token=None,
|
||||
):
|
||||
"""Create an import schedule."""
|
||||
try:
|
||||
import_time = (
|
||||
@@ -218,18 +226,27 @@ def create_import_schedule(username, request, mode, frequency, import_time, sour
|
||||
day_of_week="*" if frequency == "daily" else "*/2",
|
||||
timezone=timezone.get_default_timezone(),
|
||||
)
|
||||
|
||||
if token is None:
|
||||
kwargs = {
|
||||
"username": username,
|
||||
"user_id": request.user.id,
|
||||
"mode": mode,
|
||||
}
|
||||
else:
|
||||
kwargs = {
|
||||
"username": username,
|
||||
"user_id": request.user.id,
|
||||
"mode": mode,
|
||||
"token": token,
|
||||
}
|
||||
|
||||
# Create new periodic task
|
||||
PeriodicTask.objects.create(
|
||||
name=task_name,
|
||||
task=f"Import from {source}",
|
||||
crontab=crontab,
|
||||
kwargs=json.dumps(
|
||||
{
|
||||
"username": username,
|
||||
"user_id": request.user.id,
|
||||
"mode": mode,
|
||||
},
|
||||
),
|
||||
kwargs=json.dumps(kwargs),
|
||||
start_time=timezone.now(),
|
||||
)
|
||||
messages.success(request, f"{source} import task scheduled.")
|
||||
|
||||
@@ -35,7 +35,7 @@ def get_token(request):
|
||||
}
|
||||
|
||||
try:
|
||||
request = app.providers.services.api_request(
|
||||
token_response = app.providers.services.api_request(
|
||||
"SIMKL",
|
||||
"POST",
|
||||
url,
|
||||
@@ -48,7 +48,32 @@ def get_token(request):
|
||||
raise MediaImportError(msg) from error
|
||||
raise
|
||||
|
||||
return request["access_token"]
|
||||
return {
|
||||
"access_token": token_response["access_token"],
|
||||
"username": get_username(token_response["access_token"]),
|
||||
}
|
||||
|
||||
|
||||
def get_username(token):
|
||||
"""Get the username from SIMKL using the provided token."""
|
||||
try:
|
||||
user_info = app.providers.services.api_request(
|
||||
"SIMKL",
|
||||
"POST",
|
||||
"https://api.simkl.com/users/settings",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"simkl-api-key": settings.SIMKL_ID,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
except services.ProviderAPIError as error:
|
||||
if error.status_code == requests.codes.unauthorized:
|
||||
msg = "Invalid SIMKL secret key."
|
||||
raise MediaImportError(msg) from error
|
||||
raise
|
||||
|
||||
return user_info["account"]["id"]
|
||||
|
||||
|
||||
def importer(token, user, mode):
|
||||
|
||||
@@ -32,11 +32,11 @@ def handle_oauth_callback(request):
|
||||
"client_secret": settings.TRAKT_API_SECRET,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
"redirect_uri": f"{scheme}://{domain}/import/trakt/oauth/callback",
|
||||
"redirect_uri": f"{scheme}://{domain}/import/trakt",
|
||||
}
|
||||
|
||||
try:
|
||||
request = app.providers.services.api_request(
|
||||
token_response = app.providers.services.api_request(
|
||||
"TRAKT",
|
||||
"POST",
|
||||
url,
|
||||
@@ -50,10 +50,38 @@ def handle_oauth_callback(request):
|
||||
|
||||
return {
|
||||
"state": state,
|
||||
"refresh_token": request["refresh_token"],
|
||||
"refresh_token": token_response["refresh_token"],
|
||||
"username": get_username_from_oauth(token_response["access_token"]),
|
||||
}
|
||||
|
||||
|
||||
def get_username_from_oauth(access_token):
|
||||
"""View for getting the Trakt OAuth2 username."""
|
||||
url = "https://api.trakt.tv/users/me"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"trakt-api-version": "2",
|
||||
"trakt-api-key": settings.TRAKT_API,
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
}
|
||||
|
||||
try:
|
||||
request = app.providers.services.api_request(
|
||||
"TRAKT",
|
||||
"GET",
|
||||
url,
|
||||
headers=headers,
|
||||
)
|
||||
except services.ProviderAPIError as error:
|
||||
if error.status_code == requests.codes.unauthorized:
|
||||
msg = "Invalid Trakt secret key."
|
||||
raise MediaImportError(msg) from error
|
||||
raise
|
||||
|
||||
return request["username"]
|
||||
|
||||
|
||||
def get_access_token(refresh_token):
|
||||
"""View for getting the Trakt OAuth2 access token."""
|
||||
url = "https://api.trakt.tv/oauth/token"
|
||||
@@ -63,7 +91,7 @@ def get_access_token(refresh_token):
|
||||
"client_secret": settings.TRAKT_API_SECRET,
|
||||
"refresh_token": refresh_token,
|
||||
"grant_type": "refresh_token",
|
||||
"redirect_uri": f"{settings.BASE_URL}/import/trakt/oauth/callback",
|
||||
"redirect_uri": f"{settings.BASE_URL}/import/trakt",
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -82,16 +110,12 @@ def get_access_token(refresh_token):
|
||||
return request["access_token"]
|
||||
|
||||
|
||||
def importer(username, user, mode):
|
||||
"""Import the user's data from Trakt. DEPRECATED: Use importer_oauth instead."""
|
||||
trakt_importer = TraktImporter(username, user, mode)
|
||||
return trakt_importer.import_data()
|
||||
|
||||
|
||||
def importer_oauth(token, user, mode):
|
||||
def importer(token, user, mode, username=None):
|
||||
"""Import the user's data from Trakt using OAuth."""
|
||||
# Use "me" as Trakt username for OAuth authenticated user
|
||||
trakt_importer = TraktImporter("me", user, mode, refresh_token=token)
|
||||
if username is None:
|
||||
username = "me"
|
||||
trakt_importer = TraktImporter(username, user, mode, refresh_token=token)
|
||||
return trakt_importer.import_data()
|
||||
|
||||
|
||||
|
||||
@@ -49,12 +49,24 @@ def format_import_message(imported_counts, warning_messages=None):
|
||||
return info_message
|
||||
|
||||
|
||||
def import_media(importer_func, identifier, user_id, mode):
|
||||
def import_media(importer_func, identifier, user_id, mode, oauth_username=None):
|
||||
"""Handle the import process for different media services."""
|
||||
user = get_user_model().objects.get(id=user_id)
|
||||
|
||||
with disable_fetch_releases():
|
||||
imported_counts, warnings = importer_func(identifier, user, mode)
|
||||
if oauth_username is None:
|
||||
imported_counts, warnings = importer_func(
|
||||
identifier,
|
||||
user,
|
||||
mode,
|
||||
)
|
||||
else:
|
||||
imported_counts, warnings = importer_func(
|
||||
identifier,
|
||||
user,
|
||||
mode,
|
||||
username=oauth_username,
|
||||
)
|
||||
|
||||
events.tasks.reload_calendar.delay()
|
||||
|
||||
@@ -62,23 +74,19 @@ def import_media(importer_func, identifier, user_id, mode):
|
||||
|
||||
|
||||
@shared_task(name="Import from Trakt")
|
||||
def import_trakt(username, user_id, mode):
|
||||
"""Celery task for importing media data from Trakt. Deprecated in favor of OAuth."""
|
||||
return import_media(trakt.importer, username, user_id, mode)
|
||||
|
||||
|
||||
@shared_task(name="Import from Trakt via OAuth")
|
||||
def import_trakt_oauth(username, user_id, mode):
|
||||
"""Celery task for importing media data from Trakt using OAuth."""
|
||||
token_dec = helpers.decrypt(username)
|
||||
return import_media(trakt.importer_oauth, token_dec, user_id, mode)
|
||||
def import_trakt(user_id, mode, token=None, username=None):
|
||||
"""Celery task for importing media data from Trakt."""
|
||||
token_dec = None
|
||||
if token is not None:
|
||||
token_dec = helpers.decrypt(token)
|
||||
return import_media(trakt.importer, token_dec, user_id, mode, username)
|
||||
|
||||
|
||||
@shared_task(name="Import from SIMKL")
|
||||
def import_simkl(username, user_id, mode):
|
||||
def import_simkl(token, user_id, mode, username=None): # noqa: ARG001
|
||||
"""Celery task for importing media data from SIMKL."""
|
||||
token = helpers.decrypt(username)
|
||||
return import_media(simkl.importer, token, user_id, mode)
|
||||
token_dec = helpers.decrypt(token)
|
||||
return import_media(simkl.importer, token_dec, user_id, mode)
|
||||
|
||||
|
||||
@shared_task(name="Import from MyAnimeList")
|
||||
|
||||
@@ -3,14 +3,8 @@ from django.urls import path
|
||||
from integrations import views
|
||||
|
||||
urlpatterns = [
|
||||
# Deprecated in favor of OAuth
|
||||
path("trakt-oauth", views.trakt_oauth, name="trakt_oauth"),
|
||||
path("import/trakt", views.import_trakt, name="import_trakt"),
|
||||
path("import/trakt/oauth", views.trakt_oauth, name="trakt_oauth"),
|
||||
path(
|
||||
"import/trakt/oauth/callback",
|
||||
views.import_trakt_oauth,
|
||||
name="import_trakt_oauth",
|
||||
),
|
||||
path("simkl-oauth", views.simkl_oauth, name="simkl_oauth"),
|
||||
path("import/simkl", views.import_simkl, name="import_simkl"),
|
||||
path("import/mal", views.import_mal, name="import_mal"),
|
||||
|
||||
@@ -22,37 +22,10 @@ from integrations.webhooks import emby, jellyfin, plex
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@require_POST
|
||||
def import_trakt(request):
|
||||
"""View for importing data from Trakt. Deprecated in favor of OAuth."""
|
||||
username = request.POST.get("user")
|
||||
if not username:
|
||||
messages.error(request, "Trakt username is required.")
|
||||
return redirect("import_data")
|
||||
|
||||
mode = request.POST["mode"]
|
||||
frequency = request.POST["frequency"]
|
||||
|
||||
if frequency == "once":
|
||||
tasks.import_trakt.delay(username=username, user_id=request.user.id, mode=mode)
|
||||
messages.info(request, "The task to import media from Trakt has been queued.")
|
||||
else:
|
||||
import_time = request.POST["time"]
|
||||
helpers.create_import_schedule(
|
||||
username,
|
||||
request,
|
||||
mode,
|
||||
frequency,
|
||||
import_time,
|
||||
"Trakt",
|
||||
)
|
||||
return redirect("import_data")
|
||||
|
||||
|
||||
@require_POST
|
||||
def trakt_oauth(request):
|
||||
"""View for initiating Trakt OAuth2 authorization flow."""
|
||||
redirect_uri = request.build_absolute_uri(reverse("import_trakt_oauth"))
|
||||
redirect_uri = request.build_absolute_uri(reverse("import_trakt"))
|
||||
url = "https://trakt.tv/oauth/authorize"
|
||||
state = {
|
||||
"trakt_import_mode": request.POST["mode"],
|
||||
@@ -65,7 +38,7 @@ def trakt_oauth(request):
|
||||
|
||||
|
||||
@require_GET
|
||||
def import_trakt_oauth(request):
|
||||
def import_trakt(request):
|
||||
"""View for getting the Trakt OAuth2 token."""
|
||||
oauth_callback = trakt.handle_oauth_callback(request)
|
||||
|
||||
@@ -75,20 +48,22 @@ def import_trakt_oauth(request):
|
||||
import_time = oauth_callback["state"]["trakt_import_time"]
|
||||
|
||||
if frequency == "once":
|
||||
tasks.import_trakt_oauth.delay(
|
||||
tasks.import_trakt.delay(
|
||||
token=enc_token,
|
||||
user_id=request.user.id,
|
||||
mode=mode,
|
||||
username=oauth_callback["username"],
|
||||
)
|
||||
messages.info(request, "The task to import media from Trakt has been queued.")
|
||||
else:
|
||||
helpers.create_import_schedule(
|
||||
enc_token,
|
||||
oauth_callback["username"],
|
||||
request,
|
||||
mode,
|
||||
frequency,
|
||||
import_time,
|
||||
"Trakt via OAuth",
|
||||
"Trakt",
|
||||
token=enc_token,
|
||||
)
|
||||
return redirect("import_data")
|
||||
|
||||
@@ -112,23 +87,24 @@ def simkl_oauth(request):
|
||||
@require_GET
|
||||
def import_simkl(request):
|
||||
"""View for getting the SIMKL OAuth2 token."""
|
||||
token = simkl.get_token(request)
|
||||
enc_token = helpers.encrypt(token)
|
||||
oauth_callback = simkl.get_token(request)
|
||||
enc_token = helpers.encrypt(oauth_callback["access_token"])
|
||||
frequency = request.session.pop("simkl_import_frequency")
|
||||
mode = request.session.pop("simkl_import_mode")
|
||||
import_time = request.session.pop("simkl_import_time")
|
||||
|
||||
if frequency == "once":
|
||||
tasks.import_simkl.delay(username=enc_token, user_id=request.user.id, mode=mode)
|
||||
tasks.import_simkl.delay(token=enc_token, user_id=request.user.id, mode=mode)
|
||||
messages.info(request, "The task to import media from Simkl has been queued.")
|
||||
else:
|
||||
helpers.create_import_schedule(
|
||||
enc_token,
|
||||
oauth_callback["username"],
|
||||
request,
|
||||
mode,
|
||||
frequency,
|
||||
import_time,
|
||||
"SIMKL",
|
||||
token=enc_token,
|
||||
)
|
||||
|
||||
return redirect("import_data")
|
||||
|
||||
@@ -468,8 +468,7 @@ class User(AbstractUser):
|
||||
def get_import_tasks(self):
|
||||
"""Return import tasks history and schedules for the user."""
|
||||
import_tasks = {
|
||||
"trakt": "Import from Trakt", # Deprecated in favor of OAuth
|
||||
"trakt_oauth": "Import from Trakt via OAuth",
|
||||
"trakt": "Import from Trakt",
|
||||
"simkl": "Import from SIMKL",
|
||||
"myanimelist": "Import from MyAnimeList",
|
||||
"anilist": "Import from AniList",
|
||||
|
||||
@@ -27,10 +27,6 @@ def source_display(source_name):
|
||||
"name": "Kitsu",
|
||||
"logo": "https://kitsu.app/favicon-194x194-2f4dbec5ffe82b8f61a3c6d28a77bc6e.png",
|
||||
},
|
||||
"trakt_oauth": {
|
||||
"name": "Trakt via OAuth",
|
||||
"logo": "https://trakt.tv/assets/logos/logomark.square.gradient-b644b16c38ff775861b4b1f58c1230f6a097a2466ab33ae00445a505c33fcb91.svg",
|
||||
},
|
||||
"trakt": {
|
||||
"name": "Trakt",
|
||||
"logo": "https://trakt.tv/assets/logos/logomark.square.gradient-b644b16c38ff775861b4b1f58c1230f6a097a2466ab33ae00445a505c33fcb91.svg",
|
||||
|
||||
Reference in New Issue
Block a user