backend of "III-5 Quality filter for content"
This commit is contained in:
parent
615ebde282
commit
2c2afe0b8f
|
@ -177,6 +177,7 @@ class UploadFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
|||
size = None
|
||||
duration = None
|
||||
mimetype = "audio/ogg"
|
||||
quality = 1
|
||||
|
||||
class Meta:
|
||||
model = "music.Upload"
|
||||
|
|
|
@ -115,6 +115,11 @@ class ArtistFilter(
|
|||
)
|
||||
)
|
||||
|
||||
has_mbid = filters.BooleanFilter(
|
||||
field_name="_",
|
||||
method="filter_has_mbid",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.Artist
|
||||
fields = {
|
||||
|
@ -132,6 +137,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 +179,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 +216,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 +310,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 +342,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(
|
||||
|
|
|
@ -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),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
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 +267,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
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# this test is commented since it's very slow, but it can be useful for future development
|
||||
# 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]
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Quality filters backend (#2275)
|
Loading…
Reference in New Issue