WIP(thirdparty): allow manual trigger of the plugins hook

This commit is contained in:
Petitminion 2025-06-11 20:23:12 +02:00
parent 1e714dd28e
commit 2d0a8676e4
9 changed files with 246 additions and 14 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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"])

View File

@ -399,6 +399,7 @@ class UploadSerializer(serializers.ModelSerializer):
"import_date",
"import_status",
"privacy_level",
"third_party_provider",
]
read_only_fields = [

View File

@ -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()

View File

@ -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<components['schemas']['UploadForOwner'][]>([])
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') }}
</span>
</PopoverItem>
<PopoverItem
v-if="!playable && thirdPartyUpload"
:title="labels.thirdPartyDownloadTooltip"
icon="bi-cloud-download"
@click.stop.prevent="triggerThirdPartyHook();"
>
{{ labels.thirdPartyDownload }}
</PopoverItem>
<PopoverItem
v-else-if="!playable && !thirdPartyUpload"
:title="labels.thirdPartyDownloadTooltip"
:disabled="true"
icon="bi-cloud-download"
>
{{ labels.thirdPartyDownloadTriggered }}
</PopoverItem>
<hr v-if="filterableArtist || Object.keys(getReportableObjects({ track, album, artist, playlist, account, channel })).length > 0">
<PopoverItem

View File

@ -228,7 +228,6 @@ export default (props: PlayOptionsProps) => {
return response;
};
return {
playable,
filterableArtist,

View File

@ -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;

View File

@ -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…",