From 2d0a8676e48247b68982d6ec9b07a89f1e6af578 Mon Sep 17 00:00:00 2001 From: Petitminion Date: Wed, 11 Jun 2025 20:23:12 +0200 Subject: [PATCH] WIP(thirdparty): allow manual trigger of the plugins hook --- api/funkwhale_api/common/schema.yml | 72 +++++++++++++++ .../contrib/archivedl/funkwhale_ready.py | 2 +- api/funkwhale_api/contrib/archivedl/tasks.py | 22 +++-- api/funkwhale_api/music/serializers.py | 1 + api/funkwhale_api/music/views.py | 22 +++++ front/src/components/audio/PlayButton.vue | 48 +++++++++- front/src/composables/audio/usePlayOptions.ts | 1 - front/src/generated/types.ts | 87 +++++++++++++++++++ front/src/locales/en_US.json | 5 +- 9 files changed, 246 insertions(+), 14 deletions(-) diff --git a/api/funkwhale_api/common/schema.yml b/api/funkwhale_api/common/schema.yml index b27a21c0b..f25354a85 100644 --- a/api/funkwhale_api/common/schema.yml +++ b/api/funkwhale_api/common/schema.yml @@ -9326,6 +9326,36 @@ paths: schema: $ref: '#/components/schemas/UploadForOwner' description: '' + /api/v1/uploads/trigger-download/: + post: + operationId: create_upload_trigger_download + tags: + - uploads + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UploadForOwnerRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UploadForOwnerRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/UploadForOwnerRequest' + application/activity+json: + schema: + $ref: '#/components/schemas/UploadForOwnerRequest' + required: true + security: + - oauth2: [] + - ApplicationToken: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UploadForOwner' + description: '' /api/v1/users/{username}/: put: operationId: update_user @@ -19007,6 +19037,36 @@ paths: schema: $ref: '#/components/schemas/UploadForOwner' description: '' + /api/v2/uploads/trigger-download/: + post: + operationId: create_upload_trigger_download_2 + tags: + - uploads + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UploadForOwnerRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UploadForOwnerRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/UploadForOwnerRequest' + application/activity+json: + schema: + $ref: '#/components/schemas/UploadForOwnerRequest' + required: true + security: + - oauth2: [] + - ApplicationToken: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UploadForOwner' + description: '' /api/v2/users/{username}/: put: operationId: update_user_3 @@ -24767,6 +24827,10 @@ components: default: pending privacy_level: $ref: '#/components/schemas/LibraryPrivacyLevelEnum' + third_party_provider: + type: string + nullable: true + maxLength: 100 import_metadata: $ref: '#/components/schemas/ImportMetadataRequest' import_reference: @@ -25908,6 +25972,10 @@ components: default: pending privacy_level: $ref: '#/components/schemas/LibraryPrivacyLevelEnum' + third_party_provider: + type: string + nullable: true + maxLength: 100 import_details: readOnly: true import_metadata: @@ -25951,6 +26019,10 @@ components: default: pending privacy_level: $ref: '#/components/schemas/LibraryPrivacyLevelEnum' + third_party_provider: + type: string + nullable: true + maxLength: 100 import_metadata: $ref: '#/components/schemas/ImportMetadataRequest' import_reference: diff --git a/api/funkwhale_api/contrib/archivedl/funkwhale_ready.py b/api/funkwhale_api/contrib/archivedl/funkwhale_ready.py index 38425f3ed..5f500f408 100644 --- a/api/funkwhale_api/contrib/archivedl/funkwhale_ready.py +++ b/api/funkwhale_api/contrib/archivedl/funkwhale_ready.py @@ -9,5 +9,5 @@ logger = logging.getLogger(__name__) @plugins.register_hook(plugins.TRIGGER_THIRD_PARTY_UPLOAD, PLUGIN) -def lauch_download(track, conf={}): +def lauch_download(track, actor, conf={}): tasks.archive_download.delay(track_id=track.pk, conf=conf) diff --git a/api/funkwhale_api/contrib/archivedl/tasks.py b/api/funkwhale_api/contrib/archivedl/tasks.py index 51e0d9da4..f4bbb4cd2 100644 --- a/api/funkwhale_api/contrib/archivedl/tasks.py +++ b/api/funkwhale_api/contrib/archivedl/tasks.py @@ -53,7 +53,7 @@ def check_last_third_party_queries(track, count): check_last_third_party_queries(track, count) -def create_upload(url, track, files_data): +def get_or_create_upload(url, track, files_data): mimetype = f"audio/{files_data.get('format', 'unknown')}" duration = files_data.get("mtime", 0) filesize = files_data.get("size", 0) @@ -64,19 +64,23 @@ def create_upload(url, track, files_data): actor=actors.get_service_actor(), ) - return models.Upload.objects.create( + defaults = { + "creation_date": timezone.now(), + "duration": duration, + "size": filesize, + "bitrate": bitrate, + "import_status": "pending", + } + upload, created = models.Upload.objects.get_or_create( + defaults=defaults, mimetype=mimetype, source=url, third_party_provider="archive-dl", - creation_date=timezone.now(), track=track, - duration=duration, - size=filesize, - bitrate=bitrate, library=service_library, from_activity=None, - import_status="pending", ) + return upload @celery.app.task(name="archivedl.archive_download") @@ -89,9 +93,11 @@ def archive_download(track, conf): logger.error(e) return artist_name = utils.get_artist_credit_string(track) + # a lot of times this don't find anything, archive relies more on albums than tracks query = f"mediatype:audio AND title:{track.title} AND creator:{artist_name}" with requests.Session() as session: url = get_search_url(query, page_size=1, page=1) + get_or_create_upload(url, track, files_data={}) page_data = fetch_json(url, session) for obj in page_data["response"]["docs"]: logger.info(f"launching download item for {str(obj)}") @@ -123,7 +129,7 @@ def download_item( ) ) url = f"https://archive.org/download/{item_data['identifier']}/{to_download[0]['name']}" - upload = create_upload(url, track, to_download[0]) + upload = get_or_create_upload(url, track, to_download[0]) try: with tempfile.TemporaryDirectory() as temp_dir: path = os.path.join(temp_dir, to_download[0]["name"]) diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index df0b508d3..acf58f64b 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -399,6 +399,7 @@ class UploadSerializer(serializers.ModelSerializer): "import_date", "import_status", "privacy_level", + "third_party_provider", ] read_only_fields = [ diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index ba9336037..7f3a01336 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -18,6 +18,7 @@ from rest_framework import views, viewsets from rest_framework.decorators import action from rest_framework.response import Response +from config import plugins from funkwhale_api.common import decorators as common_decorators from funkwhale_api.common import permissions as common_permissions from funkwhale_api.common import preferences @@ -820,6 +821,27 @@ class UploadViewSet( status=200, ) + @action(methods=["post"], detail=False, url_path="trigger-download") + def trigger_download(self, request, *args, **kwargs): + qs = self.get_queryset() + track = models.Track.objects.get(pk=request.data["track"]) + actor = utils.get_actor_from_request(self.request) + tp_upload = models.Upload.objects.filter( + track=track, third_party_provider__isnull=False + ) + if tp_upload.exists(): + return Response( + serializers.UploadSerializer(tp_upload.first()).data, + status=200, + ) + if not qs.filter(track=track).exists(): + plugins.trigger_hook( + plugins.TRIGGER_THIRD_PARTY_UPLOAD, + track=track, + actor=actor, + ) + return Response(status=404) + @action(methods=["post"], detail=False) def action(self, request, *args, **kwargs): queryset = self.get_queryset() diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index b1debb5d3..3d5960f7d 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -3,7 +3,7 @@ import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/ import type { components } from '~/generated/types' import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions' -import { computed, ref } from 'vue' +import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import usePlayOptions from '~/composables/audio/usePlayOptions' import useReport from '~/composables/moderation/useReport' @@ -14,6 +14,7 @@ import Button from '~/components/ui/Button.vue' import OptionsButton from '~/components/ui/button/Options.vue' import Popover from '~/components/ui/Popover.vue' import PopoverItem from '~/components/ui/popover/PopoverItem.vue' +import axios from 'axios' interface Props extends PlayOptionsProps { split?: boolean @@ -103,10 +104,36 @@ const labels = computed(() => ({ PlaylistUploadGranted: t('components.audio.PlayButton.button.PlaylistUploadGranted'), PlaylistUploadPending:t('components.audio.PlayButton.button.PlaylistUploadPending'), PlaylistUploadNotRequest: t('components.audio.PlayButton.button.PlaylistUploadNotRequest'), - PlaylistUploadTooltip: t('components.audio.PlayButton.button.PlaylistUploadTooltip') + PlaylistUploadTooltip: t('components.audio.PlayButton.button.PlaylistUploadTooltip'), + thirdPartyDownload: t('components.audio.PlayButton.button.thirdPartyDownload'), + thirdPartyDownloadTooltip: t('components.audio.PlayButton.button.thirdPartyDownload'), + thirdPartyDownloadTriggered: t('components.audio.PlayButton.button.thirdPartyDownloadTriggered') + })) const isOpen = ref(false) +const uploads = ref([]) + +const thirdPartyUpload = computed(() => { + if (uploads.value?.find(upload => 'third_party_provider' in upload)) { return false } else { return true } +}) + +const triggerThirdPartyHook = async () => { + const response = await axios.post( + 'uploads/trigger-download', + { track: props.track?.id } + ); + + if (response.status === 404) { + uploads.value.push({ third_party_upload: "triggered" }); + return null; + } else if (response.status === 200) { + console.log("uploads 400 (response.dat ", response.data) + uploads.value.push(response.data) + return response.data; + } +}; +triggerThirdPartyHook() const playlistLibraryFollowInfo = computed(() => { const playlist = props.playlist; @@ -246,7 +273,22 @@ const playlistLibraryFollowInfo = computed(() => { {{ t('components.audio.PlayButton.button.trackDetails') }} - + + {{ labels.thirdPartyDownload }} + + + {{ labels.thirdPartyDownloadTriggered }} +
{ return response; }; - return { playable, filterableArtist, diff --git a/front/src/generated/types.ts b/front/src/generated/types.ts index 65b4a4d4b..a465a7133 100644 --- a/front/src/generated/types.ts +++ b/front/src/generated/types.ts @@ -2895,6 +2895,22 @@ export interface paths { patch: operations["partial_update_upload_bulk_update"]; trace?: never; }; + "/api/v1/uploads/trigger-download/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["create_upload_trigger_download"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/users/{username}/": { parameters: { query?: never; @@ -6019,6 +6035,22 @@ export interface paths { patch: operations["partial_update_upload_bulk_update_2"]; trace?: never; }; + "/api/v2/uploads/trigger-download/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["create_upload_trigger_download_2"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v2/users/{username}/": { parameters: { query?: never; @@ -8472,6 +8504,7 @@ export interface components { /** @default pending */ import_status: components["schemas"]["ImportStatusEnum"]; privacy_level?: components["schemas"]["LibraryPrivacyLevelEnum"]; + third_party_provider?: string | null; import_metadata?: components["schemas"]["ImportMetadataRequest"]; import_reference?: string; source?: string | null; @@ -8893,6 +8926,7 @@ export interface components { /** @default pending */ import_status: components["schemas"]["ImportStatusEnum"]; privacy_level?: components["schemas"]["LibraryPrivacyLevelEnum"]; + third_party_provider?: string | null; readonly import_details: unknown; import_metadata?: components["schemas"]["ImportMetadata"]; import_reference?: string; @@ -8907,6 +8941,7 @@ export interface components { /** @default pending */ import_status: components["schemas"]["ImportStatusEnum"]; privacy_level?: components["schemas"]["LibraryPrivacyLevelEnum"]; + third_party_provider?: string | null; import_metadata?: components["schemas"]["ImportMetadataRequest"]; import_reference?: string; source?: string | null; @@ -15758,6 +15793,32 @@ export interface operations { }; }; }; + create_upload_trigger_download: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UploadForOwnerRequest"]; + "application/x-www-form-urlencoded": components["schemas"]["UploadForOwnerRequest"]; + "multipart/form-data": components["schemas"]["UploadForOwnerRequest"]; + "application/activity+json": components["schemas"]["UploadForOwnerRequest"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadForOwner"]; + }; + }; + }; + }; update_user: { parameters: { query?: never; @@ -22917,6 +22978,32 @@ export interface operations { }; }; }; + create_upload_trigger_download_2: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UploadForOwnerRequest"]; + "application/x-www-form-urlencoded": components["schemas"]["UploadForOwnerRequest"]; + "multipart/form-data": components["schemas"]["UploadForOwnerRequest"]; + "application/activity+json": components["schemas"]["UploadForOwnerRequest"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadForOwner"]; + }; + }; + }; + }; update_user_3: { parameters: { query?: never; diff --git a/front/src/locales/en_US.json b/front/src/locales/en_US.json index 43094e895..20674d59b 100644 --- a/front/src/locales/en_US.json +++ b/front/src/locales/en_US.json @@ -492,7 +492,10 @@ "playTracks": "Play tracks", "report": "Report…", "startRadio": "Play similar songs", - "trackDetails": "Track details" + "trackDetails": "Track details", + "thirdPartyDownload": "Try to download from third-party service", + "thirdPartyDownloadTooltip": "This relies on installed plugins and may not work on all pods.", + "thirdPartyDownloadTriggered": "Download triggered, come back later" }, "title": { "more": "More…",