Porównaj commity

...

6 Commity

Autor SHA1 Wiadomość Data
petitminion ef47cc79af Merge branch '1469-quality-filter-backend' into 'develop'
backend of  "III-5 Quality filter for content"

See merge request funkwhale/funkwhale!2710
2024-04-16 12:55:01 +00:00
Petitminion c086770f97 remove comented code 2024-02-29 14:13:52 +01:00
Petitminion 857196cb26 resolve migration error and add migration test 2024-02-06 13:56:54 +01:00
Petitminion a0acdd18a1 add quality filter 2024-01-30 19:52:01 +01:00
Petitminion d94c479799 changlog 2024-01-29 20:02:49 +01:00
Petitminion 2532dbb4a5 add most quality filters and test 2024-01-29 19:53:21 +01:00
9 zmienionych plików z 406 dodań i 1 usunięć

Wyświetl plik

@ -177,6 +177,7 @@ class UploadFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
size = None
duration = None
mimetype = "audio/ogg"
quality = 1
class Meta:
model = "music.Upload"

Wyświetl plik

@ -1,6 +1,7 @@
import django_filters
from django.db.models import Q
from django_filters import rest_framework as filters
from django_filters import widgets
from funkwhale_api.audio import filters as audio_filters
from funkwhale_api.audio import models as audio_models
@ -115,6 +116,11 @@ class ArtistFilter(
)
)
has_mbid = filters.BooleanFilter(
field_name="_",
method="filter_has_mbid",
)
class Meta:
model = models.Artist
fields = {
@ -132,6 +138,9 @@ class ArtistFilter(
def filter_has_albums(self, queryset, name, value):
return queryset.filter(albums__isnull=not value)
def filter_has_mbid(self, queryset, name, value):
return queryset.filter(mbid__isnull=(not value))
class TrackFilter(
RelatedFilterSet,
@ -171,6 +180,21 @@ class TrackFilter(
("tag_matches", "related"),
)
)
format = filters.CharFilter(
field_name="_",
method="filter_format",
)
has_mbid = filters.BooleanFilter(
field_name="_",
method="filter_has_mbid",
)
quality_choices = [(0, "low"), (1, "medium"), (2, "high"), (3, "very_high")]
quality = filters.ChoiceFilter(
choices=quality_choices,
method="filter_quality",
)
class Meta:
model = models.Track
@ -193,6 +217,23 @@ class TrackFilter(
def filter_artist(self, queryset, name, value):
return queryset.filter(Q(artist=value) | Q(album__artist=value))
def filter_format(self, queryset, name, value):
mimetypes = [utils.get_type_from_ext(e) for e in value.split(",")]
return queryset.filter(uploads__mimetype__in=mimetypes)
def filter_has_mbid(self, queryset, name, value):
return queryset.filter(mbid__isnull=(not value))
def filter_quality(self, queryset, name, value):
if value == "low":
return queryset.filter(upload__quality__gte=0)
if value == "medium":
return queryset.filter(upload__quality__gte=1)
if value == "high":
return queryset.filter(upload__quality__gte=2)
if value == "very-high":
return queryset.filter(upload__quality=3)
class UploadFilter(audio_filters.IncludeChannelsFilterSet):
library = filters.CharFilter("library__uuid")
@ -270,6 +311,25 @@ class AlbumFilter(
("tag_matches", "related"),
)
)
has_tags = filters.BooleanFilter(
field_name="_",
method="filter_has_tags",
)
has_mbid = filters.BooleanFilter(
field_name="_",
method="filter_has_mbid",
)
has_cover = filters.BooleanFilter(
field_name="_",
method="filter_has_cover",
)
has_release_date = filters.BooleanFilter(
field_name="_",
method="filter_has_release_date",
)
class Meta:
model = models.Album
@ -283,6 +343,18 @@ class AlbumFilter(
actor = utils.get_actor_from_request(self.request)
return queryset.playable_by(actor, value)
def filter_has_tags(self, queryset, name, value):
return queryset.filter(tagged_items__isnull=(not value))
def filter_has_mbid(self, queryset, name, value):
return queryset.filter(mbid__isnull=(not value))
def filter_has_cover(self, queryset, name, value):
return queryset.filter(attachment_cover__isnull=(not value))
def filter_has_release_date(self, queryset, name, value):
return queryset.filter(release_date__isnull=(not value))
class LibraryFilter(filters.FilterSet):
q = fields.SearchFilter(

Wyświetl plik

@ -0,0 +1,100 @@
# Generated by Django 3.2.23 on 2024-01-30 11:58
import itertools
from django.db import migrations, models
from django.db.models import Q
from funkwhale_api.music import utils
def set_quality_upload(apps, schema_editor):
Upload = apps.get_model("music", "Upload")
extension_to_mimetypes = utils.get_extension_to_mimetype_dict()
# Low quality
mp3_query = Q(mimetype__in=extension_to_mimetypes["mp3"]) & Q(bitrate__lte=192)
OpusAACOGG_query = Q(
mimetype__in=list(
itertools.chain(
extension_to_mimetypes["opus"],
extension_to_mimetypes["ogg"],
extension_to_mimetypes["aac"],
)
)
) & Q(bitrate__lte=96)
low = Upload.objects.filter((mp3_query) | (OpusAACOGG_query))
low.update(quality=0)
# medium
mp3_query = Q(mimetype__in=extension_to_mimetypes["mp3"]) & Q(bitrate__lte=256)
ogg_query = Q(mimetype__in=extension_to_mimetypes["ogg"]) & Q(bitrate__lte=192)
aacopus_query = Q(
mimetype__in=list(
itertools.chain(
extension_to_mimetypes["aac"], extension_to_mimetypes["opus"]
)
)
) & Q(bitrate__lte=128)
medium = Upload.objects.filter((mp3_query) | (ogg_query) | (aacopus_query)).exclude(
pk__in=low.values_list("pk", flat=True)
)
medium.update(quality=1)
# high
mp3_query = Q(mimetype__in=extension_to_mimetypes["mp3"]) & Q(bitrate__lte=320)
ogg_query = Q(mimetype__in=extension_to_mimetypes["ogg"]) & Q(bitrate__lte=256)
aac_query = Q(mimetype__in=extension_to_mimetypes["aac"]) & Q(bitrate__lte=288)
opus_query = Q(mimetype__in=extension_to_mimetypes["opus"]) & Q(bitrate__lte=160)
high = (
Upload.objects.filter((mp3_query) | (ogg_query) | (aac_query) | (opus_query))
.exclude(pk__in=low.values_list("pk", flat=True))
.exclude(pk__in=medium.values_list("pk", flat=True))
)
high.update(quality=2)
# veryhigh
opus_query = Q(mimetype__in=extension_to_mimetypes["opus"]) & Q(bitrate__gte=510)
flacaifaiff_query = Q(
mimetype__in=list(
itertools.chain(
extension_to_mimetypes["flac"],
extension_to_mimetypes["aif"],
extension_to_mimetypes["aiff"],
)
)
)
Upload.objects.filter((opus_query) | (flacaifaiff_query)).exclude(
pk__in=low.values_list("pk", flat=True)
).exclude(pk__in=medium.values_list("pk", flat=True)).exclude(
pk__in=high.values_list("pk", flat=True)
).update(
quality=3
)
def skip(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("music", "0057_auto_20221118_2108"),
]
operations = [
migrations.AddField(
model_name="upload",
name="quality",
field=models.IntegerField(
choices=[(0, "low"), (1, "medium"), (2, "high"), (3, "very_high")],
default=1,
),
),
migrations.RunPython(set_quality_upload, skip),
]

Wyświetl plik

@ -1,4 +1,5 @@
import datetime
import itertools
import logging
import os
import tempfile
@ -710,6 +711,9 @@ def get_import_reference():
return str(uuid.uuid4())
quality_choices = [(0, "low"), (1, "medium"), (2, "high"), (3, "very_high")]
class Upload(models.Model):
fid = models.URLField(unique=True, max_length=500, null=True, blank=True)
uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4)
@ -768,6 +772,7 @@ class Upload(models.Model):
# stores checksums such as `sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
checksum = models.CharField(max_length=100, db_index=True, null=True, blank=True)
quality = models.IntegerField(choices=quality_choices, default=1)
objects = UploadQuerySet.as_manager()
@property
@ -871,6 +876,48 @@ class Upload(models.Model):
audio = pydub.AudioSegment.from_file(input)
return audio
def get_quality(self):
extension_to_mimetypes = utils.get_extension_to_mimetype_dict()
if not self.bitrate and self.mimetype not in list(
itertools.chain(
extension_to_mimetypes["aiff"],
extension_to_mimetypes["aif"],
extension_to_mimetypes["flac"],
)
):
return 1
bitrate_limits = {
"mp3": {192: 0, 256: 1, 320: 2},
"ogg": {96: 0, 192: 1, 256: 2},
"aac": {96: 0, 128: 1, 288: 2},
"m4a": {96: 0, 128: 1, 288: 2},
"opus": {
96: 0,
128: 1,
160: 2,
},
}
for ext in bitrate_limits:
if self.mimetype in extension_to_mimetypes[ext]:
for limit, quality in sorted(bitrate_limits[ext].items()):
if int(self.bitrate) <= limit:
return quality
# opus higher tham 160
return 3
if self.mimetype in list(
itertools.chain(
extension_to_mimetypes["aiff"],
extension_to_mimetypes["aif"],
extension_to_mimetypes["flac"],
)
):
return 3
def save(self, **kwargs):
if not self.mimetype:
if self.audio_file:
@ -890,6 +937,7 @@ class Upload(models.Model):
if not self.pk and not self.fid and self.library.actor.get_user():
self.fid = self.get_federation_id()
self.quality = self.get_quality()
return super().save(**kwargs)
def get_metadata(self):

Wyświetl plik

@ -70,6 +70,17 @@ MIMETYPE_TO_EXTENSION = {mt: ext for ext, mt in AUDIO_EXTENSIONS_AND_MIMETYPE}
SUPPORTED_EXTENSIONS = list(sorted({ext for ext, _ in AUDIO_EXTENSIONS_AND_MIMETYPE}))
def get_extension_to_mimetype_dict():
extension_dict = {}
for ext, mimetype in AUDIO_EXTENSIONS_AND_MIMETYPE:
if ext not in extension_dict:
extension_dict[ext] = []
extension_dict[ext].append(mimetype)
return extension_dict
def get_ext_from_type(mimetype):
return MIMETYPE_TO_EXTENSION.get(mimetype)

Wyświetl plik

@ -1,7 +1,10 @@
import os
import pytest
from funkwhale_api.music import filters, models
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
def test_artist_filter_ordering(factories, mocker):
# Lista de prueba
@ -263,3 +266,124 @@ def test_filter_tag_related(
queryset=obj.__class__.objects.all(),
)
assert filterset.qs == matches
@pytest.mark.parametrize(
"extension, mimetype", [("ogg", "audio/ogg"), ("mp3", "audio/mpeg")]
)
def test_track_filter_format(extension, mimetype, factories, mocker, anonymous_user):
track_expected = factories["music.Track"]()
name = ".".join(["test", extension])
path = os.path.join(DATA_DIR, name)
factories["music.Upload"](
audio_file__from_path=path, track=track_expected, mimetype=mimetype
)
track_unexpected = factories["music.Track"]()
path_wrong_ext = os.path.join(DATA_DIR, "test.m4a")
factories["music.Upload"](
audio_file__from_path=path_wrong_ext,
track=track_unexpected,
mimetype="audio/x-m4a",
)
qs = models.Track.objects.all()
filterset = filters.TrackFilter(
{"format": "ogg,mp3"},
request=mocker.Mock(user=anonymous_user),
queryset=qs,
)
assert filterset.qs[0] == track_expected
def test_album_filter_has_tags(factories, anonymous_user, mocker):
album_expected = factories["music.Album"]()
factories["music.Album"]()
factories["tags.TaggedItem"](content_object=album_expected)
qs = models.Album.objects.all()
filterset = filters.AlbumFilter(
{"has_tags": True},
request=mocker.Mock(user=anonymous_user),
queryset=qs,
)
assert filterset.qs[0] == album_expected
@pytest.mark.parametrize("fwobj", ["Album", "Track", "Artist"])
def test_filter_has_mbid(fwobj, factories, anonymous_user, mocker):
obj_expected = factories[f"music.{fwobj}"](
mbid="e9b9d574-537d-4d2d-a4c7-6f6c91eaf4e0"
)
factories[f"music.{fwobj}"](mbid=None)
model_class = getattr(models, fwobj)
qs = model_class.objects.all()
filter_class = getattr(filters, f"{fwobj}Filter")
filterset = filter_class(
data={"has_mbid": True},
request=mocker.Mock(user=anonymous_user),
queryset=qs,
)
assert filterset.qs[0] == obj_expected
@pytest.mark.parametrize(
"mimetype, bitrate, quality",
[
("audio/mpeg", "20", "low"),
("audio/ogg", "180", "medium"),
("audio/x-m4a", "280", "high"),
("audio/opus", "130", "high"),
("audio/opus", "513", "very-high"),
("audio/aiff", "1312", "very-high"),
("audio/aiff", "1312", "low"),
("audio/ogg", "180", "low"),
],
)
def test_track_quality_filter(
factories, quality, mimetype, bitrate, mocker, anonymous_user
):
track = factories["music.Track"]()
factories["music.Upload"](track=track, mimetype=mimetype, bitrate=bitrate)
factories["music.Track"]()
qs = models.Track.objects.all()
filterset = filters.TrackFilter(
{"quality": quality},
request=mocker.Mock(user=anonymous_user),
queryset=qs,
)
assert track in filterset.qs
def test_album_has_cover(factories, mocker, anonymous_user):
attachment_cover = factories["common.Attachment"]()
album = factories["music.Album"](attachment_cover=attachment_cover)
factories["music.Album"].create_batch(5)
qs = models.Album.objects.all()
filterset = filters.AlbumFilter(
{"has_cover": True},
request=mocker.Mock(user=anonymous_user),
queryset=qs,
)
assert filterset.qs[0] == album
def test_album_has_release_date(factories, mocker, anonymous_user):
album = factories["music.Album"]()
factories["music.Album"](release_date=None)
qs = models.Album.objects.all()
filterset = filters.AlbumFilter(
{"has_release_date": True},
request=mocker.Mock(user=anonymous_user),
queryset=qs,
)
assert filterset.qs[0] == album

Wyświetl plik

@ -0,0 +1,32 @@
# this test is commented since it's very slow, but it can be usefull for future developement
# def test_pytest_plugin_initial(migrator):
# mapping_list = [
# ("audio/mpeg", "20", "low", 0, 1),
# ("audio/ogg", "180", "medium", 1, 2),
# ("audio/x-m4a", "280", "high", 2, 3),
# ("audio/opus", "130", "high", 2, 4),
# ("audio/opus", "513", "very-high", 3, 5),
# ("audio/aiff", "1312", "very-high", 3, 6),
# ("audio/mpeg", "320", "high", 2, 8),
# ("audio/mpeg", "200", "medium", 1, 9),
# ("audio/aiff", "1", "very-high", 3, 10),
# ("audio/flac", "1", "very-high", 3, 11),
# ]
# a, f, t = ("music", "0057_auto_20221118_2108", "0058_upload_quality")
# migrator.migrate([(a, f)])
# old_apps = migrator.loader.project_state([(a, f)]).apps
# Upload = old_apps.get_model(a, "Upload")
# for upload in mapping_list:
# Upload.objects.create(pk=upload[4], mimetype=upload[0], bitrate=upload[1])
# migrator.loader.build_graph()
# migrator.migrate([(a, t)])
# new_apps = migrator.loader.project_state([(a, t)]).apps
# upload_manager = new_apps.get_model(a, "Upload")
# for upload in mapping_list:
# upload_obj = upload_manager.objects.get(pk=upload[4])
# assert upload_obj.quality == upload[3]

Wyświetl plik

@ -6,7 +6,7 @@ from django.utils import timezone
from funkwhale_api.common import utils as common_utils
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.music import importers, models, tasks
from funkwhale_api.music import importers, models, tasks, migrations
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
@ -702,3 +702,19 @@ def test_update_library_privacy_level_create_entries(
actor = actors[actor_name]
expected_tracks = [tracks[i] for i in expected]
assert list(models.Track.objects.playable_by(actor)) == expected_tracks
@pytest.mark.parametrize(
"mimetype, bitrate, quality",
[
("audio/mpeg", "20", 0),
("audio/ogg", "180", 1),
("audio/x-m4a", "280", 2),
("audio/opus", "130", 2),
("audio/opus", "161", 3),
("audio/flac", "1312", 3),
],
)
def test_save_upload_quality(factories, mimetype, bitrate, quality):
upload = factories["music.Upload"](mimetype=mimetype, bitrate=bitrate)
assert upload.quality == quality

Wyświetl plik

@ -0,0 +1 @@
Quality filters backend (#2275)