From 6491a946c8cf4fffaf8e5f69424483ec0ad3839c Mon Sep 17 00:00:00 2001 From: Krzysztof S Date: Sat, 13 Apr 2024 22:41:12 +0000 Subject: [PATCH] Extend OpenSubsonic support with Transcode Offset extension --- api/funkwhale_api/music/models.py | 22 +++++++++++++++------- api/funkwhale_api/music/utils.py | 5 ++++- api/funkwhale_api/music/views.py | 8 ++++---- api/funkwhale_api/subsonic/views.py | 15 ++++++++++++++- changes/changelog.d/opensubsonic.feature | 1 + 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index fca044a16..c61f69aba 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -916,14 +916,14 @@ class Upload(models.Model): # Not using reverse because this is slow return self.listen_url + "&download=false" - def get_transcoded_version(self, format, max_bitrate=None): + def get_transcoded_version(self, format, max_bitrate=None, time_offset=None): if format: mimetype = utils.EXTENSION_TO_MIMETYPE[format] else: mimetype = self.mimetype or "audio/mpeg" format = utils.MIMETYPE_TO_EXTENSION[mimetype] - existing_versions = self.versions.filter(mimetype=mimetype) + existing_versions = self.versions.filter(mimetype=mimetype, time_offset=time_offset) if max_bitrate is not None: # we don't want to transcode if a 320kbps version is available # and we're requestiong 300kbps @@ -936,18 +936,24 @@ class Upload(models.Model): # we found an existing version, no need to transcode again return existing_versions[0] - return self.create_transcoded_version(mimetype, format, bitrate=max_bitrate) + return self.create_transcoded_version(mimetype, format, bitrate=max_bitrate, time_offset=time_offset) @transaction.atomic - def create_transcoded_version(self, mimetype, format, bitrate): + def create_transcoded_version(self, mimetype, format, bitrate, time_offset): # we create the version with an empty file, then # we'll write to it f = ContentFile(b"") bitrate = min(bitrate or 320000, self.bitrate or 320000) - version = self.versions.create(mimetype=mimetype, bitrate=bitrate, size=0) + version = self.versions.create(mimetype=mimetype, bitrate=bitrate, time_offset=time_offset, size=0) + + if time_offset is not None: + time_offset_filename = "-" + str(time_offset) + else: + time_offset_filename = "" + # we keep the same name, but we update the extension new_name = ( - os.path.splitext(os.path.basename(self.audio_file.name))[0] + f".{format}" + os.path.splitext(os.path.basename(self.audio_file.name))[0] + time_offset_filename + f".{format}" ) version.audio_file.save(new_name, f) utils.transcode_audio( @@ -955,6 +961,7 @@ class Upload(models.Model): output=version.audio_file, output_format=utils.MIMETYPE_TO_EXTENSION[mimetype], bitrate=str(bitrate), + time_offset=time_offset, ) version.size = version.audio_file.size version.save(update_fields=["size"]) @@ -1002,10 +1009,11 @@ class UploadVersion(models.Model): accessed_date = models.DateTimeField(null=True, blank=True) audio_file = models.FileField(upload_to=get_file_path, max_length=255) bitrate = models.PositiveIntegerField() + time_offset = models.IntegerField(null=True, blank=True) size = models.IntegerField() class Meta: - unique_together = ("upload", "mimetype", "bitrate") + unique_together = ("upload", "mimetype", "bitrate", "time_offset") @property def filename(self) -> str: diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index a81e2cff0..da0a350ef 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -108,7 +108,10 @@ def transcode_file(input, output, input_format=None, output_format="mp3", **kwar return transcode_audio(audio, output, output_format, **kwargs) -def transcode_audio(audio, output, output_format, **kwargs): +def transcode_audio(audio, output, output_format, time_offset=None, **kwargs): + if time_offset is not None: + audio = audio[time_offset:] + with output.open("wb"): return audio.export(output, format=output_format, **kwargs) diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 282ceca24..2d89ef4c4 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -501,7 +501,7 @@ def get_file_path(audio_file): return path.encode("utf-8") -def should_transcode(upload, format, max_bitrate=None): +def should_transcode(upload, format, max_bitrate=None, time_offset=None): if not preferences.get("music__transcoding_enabled"): return False format_need_transcoding = True @@ -552,7 +552,7 @@ def record_downloads(f): @record_downloads def handle_serve( - upload, user, format=None, max_bitrate=None, proxy_media=True, download=True + upload, user, format=None, max_bitrate=None, time_offset=None, proxy_media=True, download=True ): f = upload # we update the accessed_date @@ -593,8 +593,8 @@ def handle_serve( file_path = get_file_path(f.source.replace("file://", "", 1)) mt = f.mimetype - if should_transcode(f, format, max_bitrate=max_bitrate): - transcoded_version = f.get_transcoded_version(format, max_bitrate=max_bitrate) + if should_transcode(f, format, max_bitrate=max_bitrate, time_offset=time_offset): + transcoded_version = f.get_transcoded_version(format, max_bitrate=max_bitrate, time_offset=time_offset) transcoded_version.accessed_date = now transcoded_version.save(update_fields=["accessed_date"]) f = transcoded_version diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 87edc51ed..7a3692330 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -189,7 +189,10 @@ class SubsonicViewSet(viewsets.GenericViewSet): ) def get_open_subsonic_extensions(self, request, *args, **kwargs): data = { - "openSubsonicExtensions": [{"name": "formPost", "versions": [1]}], + "openSubsonicExtensions": [ + {"name": "formPost", "versions": [1]}, + {"name": "transcodeOffset", "versions": [1]} + ], } return response.Response(data, status=200) @@ -309,6 +312,15 @@ class SubsonicViewSet(viewsets.GenericViewSet): if max_bitrate: max_bitrate = max_bitrate * 1000 + time_offset = data.get("timeOffset") + try: + time_offset = int(time_offset) or None + except (TypeError, ValueError): + time_offset = None + + if time_offset: + time_offset = time_offset * 1000 + format = data.get("format") or None if max_bitrate and not format: # specific bitrate requested, but no format specified @@ -323,6 +335,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): user=request.user, format=format, max_bitrate=max_bitrate, + time_offset=time_offset, # Subsonic clients don't expect 302 redirection unfortunately, # So we have to proxy media files proxy_media=True, diff --git a/changes/changelog.d/opensubsonic.feature b/changes/changelog.d/opensubsonic.feature index 8d28a02cb..3f7fec2db 100644 --- a/changes/changelog.d/opensubsonic.feature +++ b/changes/changelog.d/opensubsonic.feature @@ -1 +1,2 @@ Extend Subsonic API with OpenSubsonic support (#2270) +Extend OpenSubsonic support with Transcode Offset extension