Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1.4.1-upgrade-release
  • 1121-download
  • 1218-smartplaylist_backend
  • 1373-login-form-move-reset-your-password-link
  • 1381-progress-bars
  • 1481
  • 1518-update-django-allauth
  • 1645
  • 1675-widget-improperly-configured-missing-resource-id
  • 1675-widget-improperly-configured-missing-resource-id-2
  • 1704-required-props-are-not-always-passed
  • 1716-add-frontend-tests-again
  • 1749-smtp-uri-configuration
  • 1930-first-upload-in-a-batch-always-fails
  • 1976-update-documentation-links-in-readme-files
  • 2054-player-layout
  • 2063-funkwhale-connection-interrupted-every-so-often-requires-network-reset-page-refresh
  • 2091-iii-6-improve-visuals-layout
  • 2151-refused-to-load-spa-manifest-json-2
  • 2154-add-to-playlist-pop-up-hidden-by-now-playing-screen
  • 2155-can-t-see-the-episode-list-of-a-podcast-as-an-anonymous-user-with-anonymous-access-enabled
  • 2156-add-management-command-to-change-file-ref-for-in-place-imported-files-to-s3
  • 2192-clear-queue-bug-when-random-shuffle-is-enabled
  • 2205-channel-page-pagination-link-dont-working
  • 2215-custom-logger-does-not-work-at-all-with-webkit-and-blink-based-browsers
  • 2228-troi-real-world-review
  • 2274-implement-new-upload-api
  • 2303-allow-users-to-own-tagged-items
  • 2395-far-right-filter
  • 2405-front-buttont-trigger-third-party-hook
  • 2408-troi-create-missing-tracks
  • 2416-revert-library-drop
  • 2448-complete-tags
  • 2451-delete-no-user-query
  • 2452-fetch-third-party-metadata
  • 623-test
  • 653-enable-starting-embedded-player-at-a-specific-position-in-track
  • activitypub-overview
  • album-sliders
  • arne/2091-improve-visuals
  • back-option-for-edits
  • chore/2406-compose-modularity-scope
  • develop
  • develop-password-reset
  • env-file-cleanup
  • feat/2091-improve-visuals
  • fix-amd64-docker-build-gfortran
  • fix-gitpod
  • fix-plugins-dev-setup
  • fix-rate-limit-serializer
  • fix-schema-channel-metadata-choices
  • flupsi/2803-improve-visuals
  • flupsi/2804-new-upload-process
  • funkwhale-fix_pwa_manifest
  • funkwhale-petitminion-2136-bug-fix-prune-skipped-upload
  • funkwhale-ui-buttons
  • georg/add-typescript
  • gitpod/test-1866
  • global-button-experiment
  • global-buttons
  • juniorjpdj/pkg-repo
  • manage-py-reference
  • merge-review
  • minimal-python-version
  • petitminion-develop-patch-84496
  • pin-mutagen-to-1.46
  • pipenv
  • plugins
  • plugins-v2
  • plugins-v3
  • pre-release/1.3.0
  • prune_skipped_uploads_docs
  • renovate/front-all-dependencies
  • renovate/front-major-all-dependencies
  • schema-updates
  • small-gitpod-improvements
  • spectacular_schema
  • stable
  • tempArne
  • ui-buttons
  • update-frontend-dependencies
  • upload-process-spec
  • user-concept-docs
  • v2-artists
  • vite-ws-ssl-compatible
  • wip/2091-improve-visuals
  • wvffle/dependency-maintenance
  • wvffle/new-upload-process
  • wvffle/ui-rewrite-sidebar
  • wvffle/ui-sidebar
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.21
  • 0.21-rc1
  • 0.21-rc2
  • 0.21.1
  • 0.21.2
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
  • 1.0
  • 1.0-rc1
  • 1.0.1
  • 1.1
  • 1.1-rc1
  • 1.1-rc2
  • 1.1.1
  • 1.1.2
  • 1.1.3
  • 1.1.4
  • 1.2.0
  • 1.2.0-rc1
  • 1.2.0-rc2
  • 1.2.0-testing
  • 1.2.0-testing2
  • 1.2.0-testing3
  • 1.2.0-testing4
  • 1.2.1
  • 1.2.10
  • 1.2.2
  • 1.2.3
  • 1.2.4
  • 1.2.5
  • 1.2.6
  • 1.2.6-1
  • 1.2.7
  • 1.2.8
  • 1.2.9
  • 1.3.0
  • 1.3.0-rc1
  • 1.3.0-rc2
  • 1.3.0-rc3
  • 1.3.0-rc4
  • 1.3.0-rc5
  • 1.3.0-rc6
  • 1.3.1
  • 1.3.2
  • 1.3.3
  • 1.3.4
  • 1.4.0
  • 1.4.0-rc1
  • 1.4.0-rc2
  • 1.4.1
  • 2.0.0-alpha.1
  • 2.0.0-alpha.2
190 results

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
  • 1108-remove-jwt-and-switch-to-oauth-for-ui-auth
  • 1121-download
  • 1434-update-pyld
  • 1481
  • 1514-update-channels
  • 1515-update-click
  • 1518-update-django-allauth
  • album-sliders
  • back-option-for-edits
  • cherry-pick-31413fe6
  • develop
  • fix-track-table
  • heyarne/funkwhale-enhancement/linting
  • master
  • plugins
  • plugins-v2
  • plugins-v3
  • profile-menu-redesign
  • set-sast-config-1
  • set-sast-config-2
  • update-frontend-dependencies
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.21
  • 0.21-rc1
  • 0.21-rc2
  • 0.21.1
  • 0.21.2
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
  • 1.0
  • 1.0-rc1
  • 1.0.1
  • 1.1
  • 1.1-rc1
  • 1.1-rc2
  • 1.1.1
  • 1.1.2
84 results
Show changes
Showing
with 414 additions and 192 deletions
import datetime
import logging
import sys
import time
import uuid
import feedparser
import requests
from django.conf import settings
from django.db import transaction
from django.db.models import Q
from django.utils import timezone
import feedparser
import requests
import pytz
from rest_framework import serializers
from django.templatetags.static import static
from django.urls import reverse
from django.utils import timezone
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from funkwhale_api.common import locales, preferences
from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common import locales
from funkwhale_api.common import preferences
from funkwhale_api.common import session
from funkwhale_api.common import utils as common_utils
from funkwhale_api.federation import actors
from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import serializers as federation_serializers
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.moderation import mrf
from funkwhale_api.music import models as music_models
from funkwhale_api.music import serializers as music_serializers
from funkwhale_api.music import tasks
from funkwhale_api.music.serializers import COVER_WRITE_FIELD, CoverField
from funkwhale_api.tags import models as tags_models
from funkwhale_api.tags import serializers as tags_serializers
from funkwhale_api.users import serializers as users_serializers
from . import categories
from . import models
from . import categories, models
if sys.version_info < (3, 9):
from backports.zoneinfo import ZoneInfo
else:
from zoneinfo import ZoneInfo
logger = logging.getLogger(__name__)
......@@ -66,16 +68,16 @@ class ChannelMetadataSerializer(serializers.Serializer):
if child not in categories.ITUNES_CATEGORIES[parent]:
raise serializers.ValidationError(
'"{}" is not a valid subcategory for "{}"'.format(child, parent)
f'"{child}" is not a valid subcategory for "{parent}"'
)
return child
class ChannelCreateSerializer(serializers.Serializer):
name = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"])
name = serializers.CharField(max_length=federation_models.MAX_LENGTHS["ACTOR_NAME"])
username = serializers.CharField(
max_length=music_models.MAX_LENGTHS["ARTIST_NAME"],
max_length=federation_models.MAX_LENGTHS["ACTOR_NAME"],
validators=[users_serializers.ASCIIUsernameValidator()],
)
description = common_serializers.ContentSerializer(allow_null=True)
......@@ -84,7 +86,7 @@ class ChannelCreateSerializer(serializers.Serializer):
choices=music_models.ARTIST_CONTENT_CATEGORY_CHOICES
)
metadata = serializers.DictField(required=False)
cover = music_serializers.COVER_WRITE_FIELD
cover = COVER_WRITE_FIELD
def validate(self, validated_data):
existing_channels = self.context["actor"].owned_channels.count()
......@@ -135,7 +137,8 @@ class ChannelCreateSerializer(serializers.Serializer):
metadata=validated_data["metadata"],
)
channel.actor = models.generate_actor(
validated_data["username"], name=validated_data["name"],
validated_data["username"],
name=validated_data["name"],
)
channel.library = music_models.Library.objects.create(
......@@ -155,14 +158,14 @@ NOOP = object()
class ChannelUpdateSerializer(serializers.Serializer):
name = serializers.CharField(max_length=music_models.MAX_LENGTHS["ARTIST_NAME"])
name = serializers.CharField(max_length=federation_models.MAX_LENGTHS["ACTOR_NAME"])
description = common_serializers.ContentSerializer(allow_null=True)
tags = tags_serializers.TagsListField()
content_category = serializers.ChoiceField(
choices=music_models.ARTIST_CONTENT_CATEGORY_CHOICES
)
metadata = serializers.DictField(required=False)
cover = music_serializers.COVER_WRITE_FIELD
cover = COVER_WRITE_FIELD
def validate(self, validated_data):
validated_data = super().validate(validated_data)
......@@ -232,13 +235,35 @@ class ChannelUpdateSerializer(serializers.Serializer):
return ChannelSerializer(obj, context=self.context).data
class SimpleChannelArtistSerializer(serializers.Serializer):
id = serializers.IntegerField()
fid = serializers.URLField()
mbid = serializers.CharField()
name = serializers.CharField()
creation_date = serializers.DateTimeField()
modification_date = serializers.DateTimeField()
is_local = serializers.BooleanField()
content_category = serializers.CharField()
description = common_serializers.ContentSerializer(allow_null=True, required=False)
cover = CoverField(allow_null=True, required=False)
channel = serializers.UUIDField(allow_null=True, required=False)
tracks_count = serializers.SerializerMethodField(required=False)
tags = serializers.ListField(
child=serializers.CharField(), source="_prefetched_tagged_items", required=False
)
def get_tracks_count(self, o) -> int:
return getattr(o, "_tracks_count", 0)
class ChannelSerializer(serializers.ModelSerializer):
artist = serializers.SerializerMethodField()
artist = SimpleChannelArtistSerializer()
actor = serializers.SerializerMethodField()
downloads_count = serializers.SerializerMethodField()
attributed_to = federation_serializers.APIActorSerializer()
rss_url = serializers.CharField(source="get_rss_url")
url = serializers.SerializerMethodField()
subscriptions_count = serializers.SerializerMethodField()
class Meta:
model = models.Channel
......@@ -252,32 +277,46 @@ class ChannelSerializer(serializers.ModelSerializer):
"rss_url",
"url",
"downloads_count",
"subscriptions_count",
]
def get_artist(self, obj):
return music_serializers.serialize_artist_simple(obj.artist)
def to_representation(self, obj):
data = super().to_representation(obj)
if self.context.get("subscriptions_count"):
data["subscriptions_count"] = self.get_subscriptions_count(obj)
return data
def get_subscriptions_count(self, obj):
@extend_schema_field(OpenApiTypes.INT)
def get_subscriptions_count(self, obj) -> int:
return obj.actor.received_follows.exclude(approved=False).count()
def get_downloads_count(self, obj):
return getattr(obj, "_downloads_count", None)
def get_downloads_count(self, obj) -> int:
return getattr(obj, "_downloads_count", None) or 0
@extend_schema_field(federation_serializers.APIActorSerializer)
def get_actor(self, obj):
if obj.attributed_to == actors.get_service_actor():
return None
return federation_serializers.APIActorSerializer(obj.actor).data
@extend_schema_field(OpenApiTypes.URI)
def get_url(self, obj):
return obj.actor.url
class InlineSubscriptionSerializer(serializers.Serializer):
uuid = serializers.UUIDField()
channel = serializers.UUIDField(source="target__channel__uuid")
class AllSubscriptionsSerializer(serializers.Serializer):
results = InlineSubscriptionSerializer(source="*", many=True)
count = serializers.SerializerMethodField()
def get_count(self, o) -> int:
return len(o)
class SubscriptionSerializer(serializers.Serializer):
approved = serializers.BooleanField(read_only=True)
fid = serializers.URLField(read_only=True)
......@@ -310,7 +349,7 @@ def retrieve_feed(url):
except requests.exceptions.HTTPError as e:
if e.response:
raise FeedFetchException(
"Error while fetching feed: HTTP {}".format(e.response.status_code)
f"Error while fetching feed: HTTP {e.response.status_code}"
)
raise FeedFetchException("Error while fetching feed: unknown error")
except requests.exceptions.Timeout:
......@@ -318,9 +357,9 @@ def retrieve_feed(url):
except requests.exceptions.ConnectionError:
raise FeedFetchException("Error while fetching feed: connection error")
except requests.RequestException as e:
raise FeedFetchException("Error while fetching feed: {}".format(e))
raise FeedFetchException(f"Error while fetching feed: {e}")
except Exception as e:
raise FeedFetchException("Error while fetching feed: {}".format(e))
raise FeedFetchException(f"Error while fetching feed: {e}")
return response
......@@ -339,7 +378,7 @@ def get_channel_from_rss_url(url, raise_exception=False):
parsed_feed = feedparser.parse(response.text)
serializer = RssFeedSerializer(data=parsed_feed["feed"])
if not serializer.is_valid(raise_exception=raise_exception):
raise FeedFetchException("Invalid xml content: {}".format(serializer.errors))
raise FeedFetchException(f"Invalid xml content: {serializer.errors}")
# second mrf check with validated data
urls_to_check = set()
......@@ -369,9 +408,7 @@ def get_channel_from_rss_url(url, raise_exception=False):
)
)
if parsed_feed.feed.get("rights"):
track_defaults["copyright"] = parsed_feed.feed.rights[
: music_models.MAX_LENGTHS["COPYRIGHT"]
]
track_defaults["copyright"] = parsed_feed.feed.rights
for entry in entries[: settings.PODCASTS_RSS_FEED_MAX_ITEMS]:
logger.debug("Importing feed item %s", entry.id)
s = RssFeedItemSerializer(data=entry)
......@@ -509,7 +546,7 @@ class RssFeedSerializer(serializers.Serializer):
else:
artist_kwargs = {"pk": None}
actor_kwargs = {"pk": None}
preferred_username = "rssfeed-{}".format(uuid.uuid4())
preferred_username = f"rssfeed-{uuid.uuid4()}"
actor_defaults = {
"preferred_username": preferred_username,
"type": "Application",
......@@ -531,9 +568,7 @@ class RssFeedSerializer(serializers.Serializer):
**artist_kwargs,
defaults={
"attributed_to": service_actor,
"name": validated_data["title"][
: music_models.MAX_LENGTHS["ARTIST_NAME"]
],
"name": validated_data["title"],
"content_category": "podcast",
},
)
......@@ -571,7 +606,8 @@ class RssFeedSerializer(serializers.Serializer):
# create/update the channel
channel, created = models.Channel.objects.update_or_create(
pk=existing.pk if existing else None, defaults=channel_defaults,
pk=existing.pk if existing else None,
defaults=channel_defaults,
)
return channel
......@@ -588,7 +624,7 @@ class ItunesDurationField(serializers.CharField):
try:
int_parts.append(int(part))
except (ValueError, TypeError):
raise serializers.ValidationError("Invalid duration {}".format(v))
raise serializers.ValidationError(f"Invalid duration {v}")
if len(int_parts) == 2:
hours = 0
......@@ -596,7 +632,7 @@ class ItunesDurationField(serializers.CharField):
elif len(int_parts) == 3:
hours, minutes, seconds = int_parts
else:
raise serializers.ValidationError("Invalid duration {}".format(v))
raise serializers.ValidationError(f"Invalid duration {v}")
return (hours * 3600) + (minutes * 60) + seconds
......@@ -636,6 +672,7 @@ class RssFeedItemSerializer(serializers.Serializer):
links = serializers.ListField()
tags = serializers.ListField(required=False)
summary_detail = serializers.DictField(required=False)
content = serializers.ListField(required=False)
published_parsed = DummyField(required=False)
image = serializers.DictField(required=False)
......@@ -648,6 +685,16 @@ class RssFeedItemSerializer(serializers.Serializer):
"text": content,
}
def validate_content(self, v):
# TODO: Are there RSS feeds that use more than one content item?
content = v[0].get("value")
if not content:
return
return {
"content_type": v[0].get("type", "text/plain"),
"text": content,
}
def validate_image(self, v):
url = v.get("href")
if url:
......@@ -709,7 +756,7 @@ class RssFeedItemSerializer(serializers.Serializer):
else:
existing_track = (
music_models.Track.objects.filter(
uuid=expected_uuid, artist__channel=channel
uuid=expected_uuid, artist_credit__artist__channel=channel
)
.select_related("description", "attachment_cover")
.first()
......@@ -724,21 +771,16 @@ class RssFeedItemSerializer(serializers.Serializer):
{
"disc_number": validated_data.get("itunes_season", 1) or 1,
"position": validated_data.get("itunes_episode", 1) or 1,
"title": validated_data["title"][
: music_models.MAX_LENGTHS["TRACK_TITLE"]
],
"artist": channel.artist,
"title": validated_data["title"],
}
)
if "rights" in validated_data:
track_defaults["copyright"] = validated_data["rights"][
: music_models.MAX_LENGTHS["COPYRIGHT"]
]
track_defaults["copyright"] = validated_data["rights"]
if "published_parsed" in validated_data:
track_defaults["creation_date"] = datetime.datetime.fromtimestamp(
time.mktime(validated_data["published_parsed"])
).replace(tzinfo=pytz.utc)
).replace(tzinfo=ZoneInfo("UTC"))
upload_defaults = {
"source": validated_data["links"]["audio"]["source"],
......@@ -762,14 +804,30 @@ class RssFeedItemSerializer(serializers.Serializer):
# create/update the track
track, created = music_models.Track.objects.update_or_create(
**track_kwargs, defaults=track_defaults,
**track_kwargs,
defaults=track_defaults,
)
# channel only have one artist so we can safely update artist_credit
defaults = {
"artist": channel.artist,
"credit": channel.artist.name,
"joinphrase": "",
}
query = (
Q(artist=channel.artist) & Q(credit=channel.artist.name) & Q(joinphrase="")
)
artist_credit = tasks.get_best_candidate_or_create(
music_models.ArtistCredit, query, defaults, ["artist", "joinphrase"]
)
track.artist_credit.set([artist_credit[0]])
# optimisation for reducing SQL queries, because we cannot use select_related with
# update or create, so we restore the cache by hand
if existing_track:
for field in ["attachment_cover", "description"]:
cached_id_value = getattr(existing_track, "{}_id".format(field))
new_id_value = getattr(track, "{}_id".format(field))
cached_id_value = getattr(existing_track, f"{field}_id")
new_id_value = getattr(track, f"{field}_id")
if new_id_value and cached_id_value == new_id_value:
setattr(track, field, getattr(existing_track, field))
......@@ -782,6 +840,15 @@ class RssFeedItemSerializer(serializers.Serializer):
if tags:
tags_models.set_tags(track, *tags)
# "content" refers to the <content:encoded> node in the RSS feed,
# whereas "summary_detail" refers to the <description> node.
# <description> is intended to be used as a short summary and is often
# encoded merely as plain text, whereas <content:encoded> contains
# the full episode description and is generally encoded as HTML.
#
# For details, see https://www.rssboard.org/rss-profile#element-channel-item-description
summary = validated_data.get("content")
if not summary:
summary = validated_data.get("summary_detail")
if summary:
common_utils.attach_content(track, "description", summary)
......
......@@ -3,12 +3,9 @@ import urllib.parse
from django.conf import settings
from django.db.models import Q
from django.urls import reverse
from rest_framework import serializers
from funkwhale_api.common import preferences
from funkwhale_api.common import middleware
from funkwhale_api.common import utils
from funkwhale_api.common import middleware, preferences, utils
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.music import spa_views
......@@ -64,7 +61,7 @@ def channel_detail(query, redirect_to_ap):
"rel": "alternate",
"type": "application/rss+xml",
"href": obj.get_rss_url(),
"title": "{} - RSS Podcast Feed".format(obj.artist.name),
"title": f"{obj.artist.name} - RSS Podcast Feed",
},
)
......@@ -76,7 +73,7 @@ def channel_detail(query, redirect_to_ap):
"type": "application/json+oembed",
"href": (
utils.join_url(settings.FUNKWHALE_URL, reverse("api:v1:oembed"))
+ "?format=json&url={}".format(urllib.parse.quote_plus(obj_url))
+ f"?format=json&url={urllib.parse.quote_plus(obj_url)}"
),
}
)
......
......@@ -7,8 +7,7 @@ from django.utils import timezone
from funkwhale_api.taskapp import celery
from . import models
from . import serializers
from . import models, serializers
logger = logging.getLogger(__name__)
......
from rest_framework import decorators
from rest_framework import exceptions
from rest_framework import mixins
from rest_framework import permissions as rest_permissions
from rest_framework import response
from rest_framework import viewsets
from django import http
from django.db import transaction
from django.db.models import Count, Prefetch, Q, Sum
from django.utils import timezone
from drf_spectacular.utils import extend_schema, extend_schema_view, inline_serializer
from rest_framework import decorators, exceptions, mixins
from rest_framework import permissions as rest_permissions
from rest_framework import response
from rest_framework import serializers as rest_serializers
from rest_framework import viewsets
from funkwhale_api.common import locales
from funkwhale_api.common import permissions
from funkwhale_api.common import preferences
from funkwhale_api.common import locales, permissions, preferences
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common.mixins import MultipleLookupDetailMixin
from funkwhale_api.federation import actors
......@@ -27,19 +24,28 @@ from funkwhale_api.users.oauth import permissions as oauth_permissions
from . import categories, filters, models, renderers, serializers
ARTIST_PREFETCH_QS = (
music_models.Artist.objects.select_related("description", "attachment_cover",)
music_models.Artist.objects.select_related(
"description",
"attachment_cover",
)
.prefetch_related(music_views.TAG_PREFETCH)
.annotate(_tracks_count=Count("tracks"))
.annotate(_tracks_count=Count("artist_credit__tracks"))
)
class ChannelsMixin(object):
class ChannelsMixin:
def dispatch(self, request, *args, **kwargs):
if not preferences.get("audio__channels_enabled"):
return http.HttpResponse(status=405)
return super().dispatch(request, *args, **kwargs)
@extend_schema_view(
metedata_choices=extend_schema(operation_id="get_channel_metadata_choices"),
subscribe=extend_schema(operation_id="subscribe_channel"),
unsubscribe=extend_schema(operation_id="unsubscribe_channel"),
rss_subscribe=extend_schema(operation_id="subscribe_channel_rss"),
)
class ChannelViewSet(
ChannelsMixin,
MultipleLookupDetailMixin,
......@@ -91,13 +97,15 @@ class ChannelViewSet(
return serializers.ChannelSerializer
elif self.action in ["update", "partial_update"]:
return serializers.ChannelUpdateSerializer
elif self.action == "create":
return serializers.ChannelCreateSerializer
return serializers.ChannelSerializer
def get_queryset(self):
queryset = super().get_queryset()
if self.action == "retrieve":
queryset = queryset.annotate(
_downloads_count=Sum("artist__tracks__downloads_count")
_downloads_count=Sum("artist__artist_credit__tracks__downloads_count")
)
return queryset
......@@ -131,6 +139,7 @@ class ChannelViewSet(
detail=True,
methods=["post"],
permission_classes=[rest_permissions.IsAuthenticated],
serializer_class=serializers.SubscriptionSerializer,
)
def subscribe(self, request, *args, **kwargs):
object = self.get_object()
......@@ -153,6 +162,7 @@ class ChannelViewSet(
data = serializers.SubscriptionSerializer(subscription).data
return response.Response(data, status=201)
@extend_schema(responses={204: None})
@decorators.action(
detail=True,
methods=["post", "delete"],
......@@ -184,7 +194,6 @@ class ChannelViewSet(
if object.attributed_to == actors.get_service_actor():
# external feed, we redirect to the canonical one
return http.HttpResponseRedirect(object.rss_url)
uploads = (
object.library.uploads.playable_by(None)
.prefetch_related(
......@@ -192,7 +201,9 @@ class ChannelViewSet(
"track",
queryset=music_models.Track.objects.select_related(
"attachment_cover", "description"
).prefetch_related(music_views.TAG_PREFETCH,),
).prefetch_related(
music_views.TAG_PREFETCH,
),
),
)
.select_related("track__attachment_cover", "track__description")
......@@ -201,6 +212,32 @@ class ChannelViewSet(
data = serializers.rss_serialize_channel_full(channel=object, uploads=uploads)
return response.Response(data, status=200)
@extend_schema(
responses=inline_serializer(
name="MetedataChoicesSerializer",
fields={
"language": rest_serializers.ListField(
child=inline_serializer(
name="LanguageItem",
fields={
"value": rest_serializers.CharField(),
"label": rest_serializers.CharField(),
},
)
),
"itunes_category": rest_serializers.ListField(
child=inline_serializer(
name="iTunesCategoryItem",
fields={
"value": rest_serializers.CharField(),
"label": rest_serializers.CharField(),
"children": rest_serializers.CharField(),
},
)
),
},
)
)
@decorators.action(
methods=["get"],
detail=False,
......@@ -232,7 +269,9 @@ class ChannelViewSet(
if not serializer.is_valid():
return response.Response(serializer.errors, status=400)
channel = (
models.Channel.objects.filter(rss_url=serializer.validated_data["url"],)
models.Channel.objects.filter(
rss_url=serializer.validated_data["url"],
)
.order_by("id")
.first()
)
......@@ -243,7 +282,10 @@ class ChannelViewSet(
serializer.validated_data["url"]
)
except serializers.FeedFetchException as e:
return response.Response({"detail": str(e)}, status=400,)
return response.Response(
{"detail": str(e)},
status=400,
)
subscription = federation_models.Follow(actor=request.user.actor)
subscription.fid = subscription.get_federation_id()
......@@ -312,6 +354,10 @@ class SubscriptionsViewSet(
qs = super().get_queryset()
return qs.filter(actor=self.request.user.actor)
@extend_schema(
responses=serializers.AllSubscriptionsSerializer(),
operation_id="get_all_subscriptions",
)
@decorators.action(methods=["get"], detail=False)
def all(self, request, *args, **kwargs):
"""
......@@ -319,12 +365,7 @@ class SubscriptionsViewSet(
to have a performant endpoint and avoid lots of queries just to display
subscription status in the UI
"""
subscriptions = list(
self.get_queryset().values_list("uuid", "target__channel__uuid")
)
subscriptions = self.get_queryset().values("uuid", "target__channel__uuid")
payload = {
"results": [{"uuid": str(u[0]), "channel": u[1]} for u in subscriptions],
"count": len(subscriptions),
}
payload = serializers.AllSubscriptionsSerializer(subscriptions).data
return response.Response(payload, status=200)
import click
import functools
import click
@click.group()
def cli():
......
......@@ -6,7 +6,8 @@ from . import base
def handler_add_tags_from_tracks(
artists=False, albums=False,
artists=False,
albums=False,
):
result = None
if artists:
......@@ -19,7 +20,7 @@ def handler_add_tags_from_tracks(
if result is None:
click.echo(" No relevant tags found")
else:
click.echo(" Relevant tags added to {} objects".format(len(result)))
click.echo(f" Relevant tags added to {len(result)} objects")
@base.cli.group()
......
import click
import sys
from . import base
import click
from rest_framework.exceptions import ValidationError
from . import library # noqa
from . import media # noqa
from . import plugins # noqa
from . import users # noqa
from rest_framework.exceptions import ValidationError
from . import base
def invoke():
......@@ -16,7 +16,7 @@ def invoke():
except ValidationError as e:
click.secho("Invalid data:", fg="red")
for field, errors in e.detail.items():
click.secho(" {}:".format(field), fg="red")
click.secho(f" {field}:", fg="red")
for error in errors:
click.secho(" - {}".format(error), fg="red")
click.secho(f" - {error}", fg="red")
sys.exit(1)
import click
from django.core.cache import cache
from django.conf import settings
from django.core.cache import cache
from django.core.files.storage import default_storage
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
from versatileimagefield import settings as vif_settings
from versatileimagefield.image_warmer import VersatileImageFieldWarmer
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common.models import Attachment
......@@ -41,19 +39,15 @@ def generate_thumbnails(delete):
(Attachment, "file", "attachment_square"),
]
for model, attribute, key_set in MODELS:
click.echo(
"Generating thumbnails for {}.{}…".format(model._meta.label, attribute)
)
qs = model.objects.exclude(**{"{}__isnull".format(attribute): True})
click.echo(f"Generating thumbnails for {model._meta.label}.{attribute}")
qs = model.objects.exclude(**{f"{attribute}__isnull": True})
qs = qs.exclude(**{attribute: ""})
cache_key = "*{}{}*".format(
settings.MEDIA_URL, vif_settings.VERSATILEIMAGEFIELD_SIZED_DIRNAME
)
entries = cache.keys(cache_key)
if entries:
click.echo(
" Clearing {} cache entries: {}…".format(len(entries), cache_key)
)
click.echo(f" Clearing {len(entries)} cache entries: {cache_key}")
for keys in common_utils.batch(iter(entries)):
cache.delete_many(keys)
warmer = VersatileImageFieldWarmer(
......@@ -64,6 +58,4 @@ def generate_thumbnails(delete):
)
click.echo(" Creating images")
num_created, failed_to_create = warmer.warm()
click.echo(
" {} created, {} in error".format(num_created, len(failed_to_create))
)
click.echo(f" {num_created} created, {len(failed_to_create)} in error")
......@@ -5,7 +5,6 @@ import sys
import click
from django.conf import settings
from . import base
......
import click
from django.db import transaction
from funkwhale_api.federation import models as federation_models
from funkwhale_api.users import models
from funkwhale_api.users import serializers
from funkwhale_api.users import tasks
from funkwhale_api.users import models, serializers, tasks
from . import base
from . import utils
from . import base, utils
class FakeRequest(object):
class FakeRequest:
def __init__(self, session={}):
self.session = session
......@@ -42,17 +38,18 @@ def handler_create_user(
utils.logger.debug("Creating user…")
user = serializer.save(request=request)
utils.logger.debug("Setting permissions and other attributes…")
user.is_staff = is_staff
user.is_staff = is_staff or is_superuser # Always set staff if superuser is set
user.upload_quota = upload_quota
user.is_superuser = is_superuser
for permission in permissions:
if permission in models.PERMISSIONS:
utils.logger.debug("Setting %s permission to True", permission)
setattr(user, "permission_{}".format(permission), True)
setattr(user, f"permission_{permission}", True)
else:
utils.logger.warn("Unknown permission %s", permission)
utils.logger.debug("Creating actor…")
user.actor = models.create_actor(user)
models.create_user_libraries(user)
user.save()
return user
......@@ -60,7 +57,7 @@ def handler_create_user(
@transaction.atomic
def handler_delete_user(usernames, soft=True):
for username in usernames:
click.echo("Deleting {}…".format(username))
click.echo(f"Deleting {username}")
actor = None
user = None
try:
......@@ -157,13 +154,16 @@ def users():
type=click.INT,
)
@click.option(
"--superuser/--no-superuser", default=False,
"--superuser/--no-superuser",
default=False,
)
@click.option(
"--staff/--no-staff", default=False,
"--staff/--no-staff",
default=False,
)
@click.option(
"--permission", multiple=True,
"--permission",
multiple=True,
)
def create(username, password, email, superuser, staff, permission, upload_quota):
"""Create a new user"""
......@@ -179,9 +179,9 @@ def create(username, password, email, superuser, staff, permission, upload_quota
permissions=permission,
upload_quota=upload_quota,
)
click.echo("User {} created!".format(user.username))
click.echo(f"User {user.username} created!")
if generated_password:
click.echo(" Generated password: {}".format(generated_password))
click.echo(f" Generated password: {generated_password}")
@base.delete_command(group=users, id_var="username")
......@@ -210,7 +210,9 @@ def delete(username, hard):
@click.option("--permission-settings/--no-permission-settings", default=None)
@click.option("--password", default=None, envvar="FUNKWHALE_CLI_USER_UPDATE_PASSWORD")
@click.option(
"-q", "--upload-quota", type=click.INT,
"-q",
"--upload-quota",
type=click.INT,
)
def update(username, **kwargs):
"""Update attributes for given users"""
......
from django.contrib.admin import register as initial_register, site, ModelAdmin # noqa
from django.contrib.admin import site # noqa: F401
from django.contrib.admin import ModelAdmin
from django.contrib.admin import register as initial_register
from django.db.models.fields.related import RelatedField
from . import models
from . import tasks
from . import models, tasks
def register(model):
......
......@@ -3,8 +3,7 @@ from django.conf import settings
from config import plugins
from . import mutations
from . import utils
from . import mutations, utils
class CommonConfig(AppConfig):
......
from django.conf import settings
from django.utils.translation import ugettext as _
from allauth.account.models import EmailAddress
from django.core.cache import cache
from allauth.account.utils import send_email_confirmation
from django.utils.translation import gettext as _
from oauth2_provider.contrib.rest_framework.authentication import (
OAuth2Authentication as BaseOAuth2Authentication,
)
......@@ -12,14 +9,6 @@ from rest_framework import exceptions
from funkwhale_api.users import models as users_models
def should_verify_email(user):
if user.is_superuser:
return False
has_unverified_email = not user.has_verified_primary_email
mandatory_verification = settings.ACCOUNT_EMAIL_VERIFICATION != "optional"
return has_unverified_email and mandatory_verification
class UnverifiedEmail(Exception):
def __init__(self, user):
self.user = user
......@@ -27,13 +16,17 @@ class UnverifiedEmail(Exception):
def resend_confirmation_email(request, user):
THROTTLE_DELAY = 500
cache_key = "auth:resent-email-confirmation:{}".format(user.pk)
cache_key = f"auth:resent-email-confirmation:{user.pk}"
if cache.get(cache_key):
return False
done = send_email_confirmation(request, user)
# We do the sending of the conformation by hand because we don't want to pass the request down
# to the email rendering, which would cause another UnverifiedEmail Exception and restarts the sending
# again and again
email = EmailAddress.objects.get_for_user(user, user.email)
email.send_confirmation()
cache.set(cache_key, True, THROTTLE_DELAY)
return done
return True
class OAuth2Authentication(BaseOAuth2Authentication):
......@@ -45,7 +38,7 @@ class OAuth2Authentication(BaseOAuth2Authentication):
resend_confirmation_email(request, e.user)
class ApplicationTokenAuthentication(object):
class ApplicationTokenAuthentication:
def authenticate(self, request):
try:
header = request.headers["Authorization"]
......@@ -68,7 +61,7 @@ class ApplicationTokenAuthentication(object):
msg = _("User account is disabled.")
raise exceptions.AuthenticationFailed(msg)
if should_verify_email(user):
if user.should_verify_email():
raise UnverifiedEmail(user)
request.scopes = application.scope.split()
......
......@@ -19,6 +19,10 @@ class JsonAuthConsumer(JsonWebsocketConsumer):
channels.group_add(group, self.channel_name)
def disconnect(self, close_code):
if self.scope.get("user", False) and self.scope.get("user").pk is not None:
groups = self.scope["user"].get_channels_groups() + self.groups
else:
groups = self.groups
for group in groups:
channels.group_discard(group, self.channel_name)
from django.db import transaction
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import decorators, exceptions, response, status
from rest_framework import decorators
from rest_framework import exceptions
from rest_framework import response
from rest_framework import status
from . import filters
from . import models
from . import filters, models
from . import mutations as common_mutations
from . import serializers
from . import signals
from . import tasks
from . import utils
from . import serializers, signals, tasks, utils
def action_route(serializer_class):
......@@ -87,6 +80,16 @@ def mutations_route(types):
)
return response.Response(serializer.data, status=status.HTTP_201_CREATED)
return decorators.action(
return extend_schema(
methods=["post"], responses=serializers.APIMutationSerializer()
)(
extend_schema(
methods=["get"],
responses=serializers.APIMutationSerializer(many=True),
parameters=[OpenApiParameter("id", location="query", exclude=True)],
)(
decorators.action(
methods=["get", "post"], detail=True, required_scope="edits"
)(mutations)
)
)
import factory
from funkwhale_api.factories import registry, NoUpdateOnCreate
from funkwhale_api.factories import NoUpdateOnCreate, registry
from funkwhale_api.federation import factories as federation_factories
......
......@@ -2,7 +2,6 @@ import django_filters
from django import forms
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from rest_framework import serializers
from . import search
......@@ -25,9 +24,22 @@ def privacy_level_query(user, lookup_field="privacy_level", user_field="user"):
if user.is_anonymous:
return models.Q(**{lookup_field: "everyone"})
return models.Q(
**{"{}__in".format(lookup_field): ["instance", "everyone"]}
) | models.Q(**{lookup_field: "me", user_field: user})
followers_query = models.Q(
**{
f"{lookup_field}": "followers",
f"{user_field}__actor__in": user.actor.get_approved_followings(),
}
)
# Federated TrackFavorite don't have an user associated with the trackfavorite.actor
# to do : if we implement the followers privacy_level this will become a problem
no_user_query = models.Q(**{f"{user_field}__isnull": True})
return (
models.Q(**{f"{lookup_field}__in": ["instance", "everyone"]})
| models.Q(**{lookup_field: "me", user_field: user})
| followers_query
| no_user_query
)
class SearchFilter(django_filters.CharFilter):
......@@ -58,7 +70,7 @@ class SmartSearchFilter(django_filters.CharFilter):
return qs
try:
cleaned = self.config.clean(value)
except (forms.ValidationError):
except forms.ValidationError:
return qs.none()
return search.apply(qs, cleaned)
......@@ -98,7 +110,7 @@ def get_generic_filter_query(value, relation_name, choices):
obj = related_queryset.get(obj_query)
except related_queryset.model.DoesNotExist:
raise forms.ValidationError("Invalid object")
filter_query &= models.Q(**{"{}_id".format(relation_name): obj.id})
filter_query &= models.Q(**{f"{relation_name}_id": obj.id})
return filter_query
......@@ -164,7 +176,7 @@ class GenericRelation(serializers.JSONField):
id_value = v[id_attr]
id_value = id_field.to_internal_value(id_value)
except (TypeError, KeyError, serializers.ValidationError):
raise serializers.ValidationError("Invalid {}".format(id_attr))
raise serializers.ValidationError(f"Invalid {id_attr}")
query_getter = conf.get(
"get_query", lambda attr, value: models.Q(**{attr: value})
......
from django import forms
from django.db.models import Q
from django_filters import widgets
from django.db.models.functions import Lower
from django_filters import rest_framework as filters
from django_filters import widgets
from drf_spectacular.utils import extend_schema_field
from . import fields
from . import models
from . import search
from . import utils
from . import fields, models, search, utils
class NoneObject(object):
class NoneObject:
def __eq__(self, other):
return other.__class__ == NoneObject
......@@ -49,9 +47,10 @@ class CoerceChoiceField(forms.ChoiceField):
try:
return [b for a, b in self.choices if v == a][0]
except IndexError:
raise forms.ValidationError("Invalid value {}".format(value))
raise forms.ValidationError(f"Invalid value {value}")
@extend_schema_field(bool)
class NullBooleanFilter(filters.ChoiceFilter):
field_class = CoerceChoiceField
......@@ -65,9 +64,7 @@ class NullBooleanFilter(filters.ChoiceFilter):
return qs
if value == NONE:
value = None
qs = self.get_method(qs)(
**{"%s__%s" % (self.field_name, self.lookup_expr): value}
)
qs = self.get_method(qs)(**{f"{self.field_name}__{self.lookup_expr}": value})
return qs.distinct() if self.distinct else qs
......@@ -123,7 +120,6 @@ class MultipleQueryFilter(filters.TypedMultipleChoiceFilter):
def filter_target(value):
config = {
"artist": ["artist", "target_id", int],
"album": ["album", "target_id", int],
......@@ -181,7 +177,6 @@ class ActorScopeFilter(filters.CharFilter):
super().__init__(*args, **kwargs)
def filter(self, queryset, value):
if not value:
return queryset
......@@ -219,7 +214,7 @@ class ActorScopeFilter(filters.CharFilter):
if not self.library_field:
predicate = "pk__in"
else:
predicate = "{}__in".format(self.library_field)
predicate = f"{self.library_field}__in"
return Q(**{predicate: followed_libraries})
elif scope.startswith("actor:"):
......@@ -227,7 +222,8 @@ class ActorScopeFilter(filters.CharFilter):
username, domain = full_username.split("@")
try:
actor = federation_models.Actor.objects.get(
preferred_username__iexact=username, domain_id=domain,
preferred_username__iexact=username,
domain_id=domain,
)
except federation_models.Actor.DoesNotExist:
raise EmptyQuerySet()
......@@ -235,7 +231,7 @@ class ActorScopeFilter(filters.CharFilter):
return Q(**{self.actor_field: actor})
elif scope.startswith("domain:"):
domain = scope.split("domain:", 1)[1]
return Q(**{"{}__domain_id".format(self.actor_field): domain})
return Q(**{f"{self.actor_field}__domain_id": domain})
else:
raise EmptyQuerySet()
......@@ -244,3 +240,19 @@ class ActorScopeFilter(filters.CharFilter):
raise EmptyQuerySet()
return Q(**{self.actor_field: actor})
class CaseInsensitiveNameOrderingFilter(filters.OrderingFilter):
def filter(self, qs, value):
order_by = []
if value is None:
return qs
for param in value:
if param == "name":
order_by.append(Lower("name"))
else:
order_by.append(self.get_ordering_value(param))
return qs.order_by(*order_by)
from django.conf import settings
from django.contrib.auth.management.commands.createsuperuser import (
Command as BaseCommand,
)
from django.core.management.base import CommandError
class Command(BaseCommand):
def handle(self, *apps_label, **options):
"""
Creating Django Superusers would bypass some of our username checks, which can lead to unexpected behaviour.
We therefore prohibit the execution of the command.
"""
force = settings.FORCE
if not force == 1:
raise CommandError(
"Running createsuperuser on your Funkwhale instance bypasses some of our checks "
"which can lead to unexpected behavior of your instance. We therefore suggest to "
"run `funkwhale-manage fw users create --superuser` instead."
)
return super().handle(*apps_label, **options)
import os
import debugpy
import uvicorn
from django.core.management import call_command
from django.core.management.commands.migrate import Command as BaseCommand
from funkwhale_api.common import preferences
from funkwhale_api.music.models import Library
from funkwhale_api.users.models import User
class Command(BaseCommand):
help = "Manage gitpod environment"
def add_arguments(self, parser):
parser.add_argument("command", nargs="?", type=str)
def handle(self, *args, **options):
command = options["command"]
if not command:
return self.show_help()
if command == "init":
return self.init()
if command == "dev":
return self.dev()
def show_help(self):
self.stdout.write("")
self.stdout.write("Available commands:")
self.stdout.write("init - Initialize gitpod workspace")
self.stdout.write("dev - Run Funkwhale in development mode with debug server")
self.stdout.write("")
def init(self):
user = User.objects.get(username="gitpod")
# Allow anonymous access
preferences.set("common__api_authentication_required", False)
# Download music catalog
os.system(
"git clone https://dev.funkwhale.audio/funkwhale/catalog.git /tmp/catalog"
)
os.system("mv -f /tmp/catalog/music /workspace/funkwhale/data")
os.system("rm -rf /tmp/catalog/music")
# Import music catalog into library
call_command(
"create_library",
"gitpod",
name="funkwhale/catalog",
privacy_level="everyone",
)
call_command(
"import_files",
Library.objects.get(actor=user.actor).uuid,
"/workspace/funkwhale/data/music/",
recursive=True,
in_place=True,
no_input=False,
)
def dev(self):
debugpy.listen(5678)
uvicorn.run(
"config.asgi:application",
host="0.0.0.0",
port=5000,
reload=True,
reload_dirs=[
"/workspace/funkwhale/api/config/",
"/workspace/funkwhale/api/funkwhale_api/",
],
)