音楽で楽しみましょう!-Let's have fun with music!-
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
This commit is contained in:
parent
7c3206bf83
commit
54c6d22102
517 changed files with 637 additions and 639 deletions
0
api/funquail_api/instance/__init__.py
Normal file
0
api/funquail_api/instance/__init__.py
Normal file
8
api/funquail_api/instance/consumers.py
Normal file
8
api/funquail_api/instance/consumers.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from funkwhale_api.common.consumers import JsonAuthConsumer
|
||||
|
||||
|
||||
class InstanceActivityConsumer(JsonAuthConsumer):
|
||||
groups = ["instance_activity"]
|
||||
|
||||
def event_send(self, message):
|
||||
self.send_json(message["data"])
|
||||
188
api/funquail_api/instance/dynamic_preferences_registry.py
Normal file
188
api/funquail_api/instance/dynamic_preferences_registry.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import pycountry
|
||||
from django.core.validators import FileExtensionValidator
|
||||
from django.forms import widgets
|
||||
from dynamic_preferences import types
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
|
||||
instance = types.Section("instance")
|
||||
ui = types.Section("ui")
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceName(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "name"
|
||||
default = ""
|
||||
verbose_name = "Public name"
|
||||
help_text = "The public name of your instance, displayed in the about page."
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceShortDescription(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "short_description"
|
||||
default = ""
|
||||
verbose_name = "Short description"
|
||||
help_text = "Instance succinct description, displayed in the about page."
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceLongDescription(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "long_description"
|
||||
verbose_name = "Long description"
|
||||
default = ""
|
||||
help_text = "Instance long description, displayed in the about page."
|
||||
widget = widgets.Textarea
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceTerms(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "terms"
|
||||
verbose_name = "Terms of service"
|
||||
default = ""
|
||||
help_text = "Terms of service and privacy policy for your instance."
|
||||
widget = widgets.Textarea
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceRules(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "rules"
|
||||
verbose_name = "Rules"
|
||||
default = ""
|
||||
help_text = "Rules/Code of Conduct."
|
||||
widget = widgets.Textarea
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceContactEmail(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "contact_email"
|
||||
verbose_name = "Contact email"
|
||||
default = ""
|
||||
help_text = "A contact e-mail address for visitors who need to contact an admin or moderator"
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceSupportMessage(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "support_message"
|
||||
verbose_name = "Support message"
|
||||
default = ""
|
||||
help_text = (
|
||||
"A short message that will be displayed periodically to local users. "
|
||||
"Use it to ask for financial support or anything else you might need. "
|
||||
"(markdown allowed)."
|
||||
)
|
||||
widget = widgets.Textarea
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceFunQuailSupportMessageEnabled(types.BooleanPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "funkwhale_support_message_enabled"
|
||||
verbose_name = "FunQuail Support message"
|
||||
default = True
|
||||
help_text = (
|
||||
"If this is enabled, we will periodically display a message to encourage "
|
||||
"local users to support FunQuail."
|
||||
)
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceNodeinfoPrivate(types.BooleanPreference):
|
||||
show_in_api = False
|
||||
section = instance
|
||||
name = "nodeinfo_private"
|
||||
default = False
|
||||
verbose_name = "Private mode in nodeinfo"
|
||||
help_text = (
|
||||
"Indicate in the nodeinfo endpoint that you do not want your instance "
|
||||
"to be tracked by third-party services. "
|
||||
"There is no guarantee these tools will honor this setting though."
|
||||
)
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceNodeinfoStatsEnabled(types.BooleanPreference):
|
||||
show_in_api = False
|
||||
section = instance
|
||||
name = "nodeinfo_stats_enabled"
|
||||
default = True
|
||||
verbose_name = "Enable usage and library stats in nodeinfo endpoint"
|
||||
help_text = (
|
||||
"Disable this if you don't want to share usage and library statistics "
|
||||
"in the nodeinfo endpoint but don't want to disable it completely."
|
||||
)
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class CustomCSS(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = ui
|
||||
name = "custom_css"
|
||||
verbose_name = "Custom CSS code"
|
||||
default = ""
|
||||
help_text = (
|
||||
"Custom CSS code, to be included in a <style> tag on all pages. "
|
||||
"Loading third-party resources such as fonts or images can affect the performance "
|
||||
"of the app and the privacy of your users."
|
||||
)
|
||||
widget = widgets.Textarea
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
class ImageWidget(widgets.ClearableFileInput):
|
||||
pass
|
||||
|
||||
|
||||
class ImagePreference(types.FilePreference):
|
||||
widget = ImageWidget
|
||||
field_kwargs = {
|
||||
"validators": [
|
||||
FileExtensionValidator(allowed_extensions=["png", "jpg", "jpeg", "webp"])
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class Banner(ImagePreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "banner"
|
||||
verbose_name = "Banner image"
|
||||
default = None
|
||||
help_text = "This banner will be displayed on your pod's landing and about page. At least 600x100px recommended."
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class Location(types.ChoicePreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = "location"
|
||||
verbose_name = "Server Location"
|
||||
default = ""
|
||||
choices = [(country.alpha_2, country.name) for country in pycountry.countries]
|
||||
help_text = (
|
||||
"The country or territory in which your server is located. This is displayed in the server's Nodeinfo "
|
||||
"endpoint."
|
||||
)
|
||||
field_kwargs = {"choices": choices, "required": False}
|
||||
48
api/funquail_api/instance/pwa-manifest.json
Normal file
48
api/funquail_api/instance/pwa-manifest.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "FunQuail",
|
||||
"categories": ["music", "entertainment"],
|
||||
"short_name": "FunQuail",
|
||||
"description": "Your free and federated audio platform",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"prefer_related_applications": true,
|
||||
"related_applications": [
|
||||
{
|
||||
"platform": "play",
|
||||
"url": "https://play.google.com/store/apps/details?id=audio.funkwhale.ffa",
|
||||
"id": "audio.funkwhale.ffa"
|
||||
},
|
||||
{
|
||||
"platform": "f-droid",
|
||||
"url": "https://f-droid.org/en/packages/audio.funkwhale.ffa/",
|
||||
"id": "audio.funkwhale.ffa"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Search",
|
||||
"url": "/search",
|
||||
"icons": []
|
||||
},
|
||||
{
|
||||
"name": "Library",
|
||||
"url": "/library",
|
||||
"icons": []
|
||||
},
|
||||
{
|
||||
"name": "Channels",
|
||||
"url": "/subscriptions",
|
||||
"icons": []
|
||||
}
|
||||
]
|
||||
}
|
||||
312
api/funquail_api/instance/serializers.py
Normal file
312
api/funquail_api/instance/serializers.py
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.federation.utils import full_url
|
||||
|
||||
|
||||
class SoftwareSerializer(serializers.Serializer):
|
||||
name = serializers.SerializerMethodField()
|
||||
version = serializers.CharField()
|
||||
|
||||
def get_name(self, obj) -> str:
|
||||
return "funkwhale"
|
||||
|
||||
|
||||
class SoftwareSerializer_v2(SoftwareSerializer):
|
||||
repository = serializers.SerializerMethodField()
|
||||
homepage = serializers.SerializerMethodField()
|
||||
|
||||
def get_repository(self, obj):
|
||||
return "https://dev.funkwhale.audio/funkwhale/funkwhale"
|
||||
|
||||
def get_homepage(self, obj):
|
||||
return "https://funquail.laidback.moe"
|
||||
|
||||
|
||||
class ServicesSerializer(serializers.Serializer):
|
||||
inbound = serializers.ListField(child=serializers.CharField(), default=[])
|
||||
outbound = serializers.ListField(child=serializers.CharField(), default=[])
|
||||
|
||||
|
||||
class UsersUsageSerializer(serializers.Serializer):
|
||||
total = serializers.IntegerField()
|
||||
activeHalfyear = serializers.SerializerMethodField()
|
||||
activeMonth = serializers.SerializerMethodField()
|
||||
|
||||
def get_activeHalfyear(self, obj) -> int:
|
||||
return obj.get("active_halfyear", 0)
|
||||
|
||||
def get_activeMonth(self, obj) -> int:
|
||||
return obj.get("active_month", 0)
|
||||
|
||||
|
||||
class UsageSerializer(serializers.Serializer):
|
||||
users = UsersUsageSerializer()
|
||||
localPosts = serializers.IntegerField(required=False)
|
||||
localComments = serializers.IntegerField(required=False)
|
||||
|
||||
|
||||
class TotalCountSerializer(serializers.Serializer):
|
||||
total = serializers.SerializerMethodField()
|
||||
|
||||
def get_total(self, obj) -> int:
|
||||
return obj
|
||||
|
||||
|
||||
class TotalHoursSerializer(serializers.Serializer):
|
||||
hours = serializers.SerializerMethodField()
|
||||
|
||||
def get_hours(self, obj) -> int:
|
||||
return obj
|
||||
|
||||
|
||||
class NodeInfoLibrarySerializer(serializers.Serializer):
|
||||
federationEnabled = serializers.BooleanField()
|
||||
anonymousCanListen = serializers.BooleanField()
|
||||
tracks = TotalCountSerializer(default=0)
|
||||
artists = TotalCountSerializer(default=0)
|
||||
albums = TotalCountSerializer(default=0)
|
||||
music = TotalHoursSerializer(source="music_duration", default=0)
|
||||
|
||||
|
||||
class AllowListStatSerializer(serializers.Serializer):
|
||||
enabled = serializers.BooleanField()
|
||||
domains = serializers.ListField(child=serializers.CharField())
|
||||
|
||||
|
||||
class ReportTypeSerializer(serializers.Serializer):
|
||||
type = serializers.CharField()
|
||||
label = serializers.CharField()
|
||||
anonymous = serializers.BooleanField()
|
||||
|
||||
|
||||
class EndpointsSerializer(serializers.Serializer):
|
||||
knownNodes = serializers.URLField(default=None)
|
||||
channels = serializers.URLField(default=None)
|
||||
libraries = serializers.URLField(default=None)
|
||||
|
||||
|
||||
class MetadataUsageFavoriteSerializer(serializers.Serializer):
|
||||
tracks = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(TotalCountSerializer)
|
||||
def get_tracks(self, obj):
|
||||
return TotalCountSerializer(obj).data
|
||||
|
||||
|
||||
class MetadataUsageSerializer(serializers.Serializer):
|
||||
favorites = MetadataUsageFavoriteSerializer(source="track_favorites")
|
||||
listenings = TotalCountSerializer()
|
||||
downloads = TotalCountSerializer()
|
||||
|
||||
|
||||
class MetadataSerializer(serializers.Serializer):
|
||||
actorId = serializers.CharField()
|
||||
private = serializers.SerializerMethodField()
|
||||
shortDescription = serializers.SerializerMethodField()
|
||||
longDescription = serializers.SerializerMethodField()
|
||||
contactEmail = serializers.SerializerMethodField()
|
||||
nodeName = serializers.SerializerMethodField()
|
||||
banner = serializers.SerializerMethodField()
|
||||
defaultUploadQuota = serializers.SerializerMethodField()
|
||||
supportedUploadExtensions = serializers.ListField(child=serializers.CharField())
|
||||
allowList = serializers.SerializerMethodField()
|
||||
funkwhaleSupportMessageEnabled = serializers.SerializerMethodField()
|
||||
instanceSupportMessage = serializers.SerializerMethodField()
|
||||
usage = MetadataUsageSerializer(source="stats", required=False)
|
||||
|
||||
def get_private(self, obj) -> bool:
|
||||
return obj["preferences"].get("instance__nodeinfo_private")
|
||||
|
||||
def get_shortDescription(self, obj) -> str:
|
||||
return obj["preferences"].get("instance__short_description")
|
||||
|
||||
def get_longDescription(self, obj) -> str:
|
||||
return obj["preferences"].get("instance__long_description")
|
||||
|
||||
def get_contactEmail(self, obj) -> str:
|
||||
return obj["preferences"].get("instance__contact_email")
|
||||
|
||||
def get_nodeName(self, obj) -> str:
|
||||
return obj["preferences"].get("instance__name")
|
||||
|
||||
@extend_schema_field(serializers.CharField)
|
||||
def get_banner(self, obj) -> (str, None):
|
||||
if obj["preferences"].get("instance__banner"):
|
||||
return full_url(obj["preferences"].get("instance__banner").url)
|
||||
return None
|
||||
|
||||
def get_defaultUploadQuota(self, obj) -> int:
|
||||
return obj["preferences"].get("users__upload_quota")
|
||||
|
||||
@extend_schema_field(AllowListStatSerializer)
|
||||
def get_allowList(self, obj):
|
||||
return AllowListStatSerializer(
|
||||
{
|
||||
"enabled": obj["preferences"].get("moderation__allow_list_enabled"),
|
||||
"domains": obj["allowed_domains"] or None,
|
||||
}
|
||||
).data
|
||||
|
||||
def get_funkwhaleSupportMessageEnabled(self, obj) -> bool:
|
||||
return obj["preferences"].get("instance__funkwhale_support_message_enabled")
|
||||
|
||||
def get_instanceSupportMessage(self, obj) -> str:
|
||||
return obj["preferences"].get("instance__support_message")
|
||||
|
||||
@extend_schema_field(MetadataUsageSerializer)
|
||||
def get_usage(self, obj):
|
||||
return MetadataUsageSerializer(obj["stats"]).data
|
||||
|
||||
|
||||
class Metadata20Serializer(MetadataSerializer):
|
||||
library = serializers.SerializerMethodField()
|
||||
reportTypes = ReportTypeSerializer(source="report_types", many=True)
|
||||
endpoints = EndpointsSerializer()
|
||||
rules = serializers.SerializerMethodField()
|
||||
terms = serializers.SerializerMethodField()
|
||||
|
||||
def get_rules(self, obj) -> str:
|
||||
return obj["preferences"].get("instance__rules")
|
||||
|
||||
def get_terms(self, obj) -> str:
|
||||
return obj["preferences"].get("instance__terms")
|
||||
|
||||
@extend_schema_field(NodeInfoLibrarySerializer)
|
||||
def get_library(self, obj):
|
||||
data = obj["stats"] or {}
|
||||
data["federationEnabled"] = obj["preferences"].get("federation__enabled")
|
||||
data["anonymousCanListen"] = not obj["preferences"].get(
|
||||
"common__api_authentication_required"
|
||||
)
|
||||
return NodeInfoLibrarySerializer(data).data
|
||||
|
||||
|
||||
class MetadataContentLocalSerializer(serializers.Serializer):
|
||||
artists = serializers.IntegerField()
|
||||
releases = serializers.IntegerField()
|
||||
recordings = serializers.IntegerField()
|
||||
hoursOfContent = serializers.IntegerField()
|
||||
|
||||
|
||||
class MetadataContentCategorySerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
count = serializers.IntegerField()
|
||||
|
||||
|
||||
class MetadataContentSerializer(serializers.Serializer):
|
||||
local = MetadataContentLocalSerializer()
|
||||
topMusicCategories = MetadataContentCategorySerializer(many=True)
|
||||
topPodcastCategories = MetadataContentCategorySerializer(many=True)
|
||||
|
||||
|
||||
class Metadata21Serializer(MetadataSerializer):
|
||||
languages = serializers.ListField(child=serializers.CharField())
|
||||
location = serializers.CharField()
|
||||
content = MetadataContentSerializer()
|
||||
features = serializers.ListField(child=serializers.CharField())
|
||||
codeOfConduct = serializers.SerializerMethodField()
|
||||
|
||||
def get_codeOfConduct(self, obj) -> str:
|
||||
return (
|
||||
full_url("/about/pod#rules")
|
||||
if obj["preferences"].get("instance__rules")
|
||||
else ""
|
||||
)
|
||||
|
||||
|
||||
class NodeInfo20Serializer(serializers.Serializer):
|
||||
version = serializers.SerializerMethodField()
|
||||
software = SoftwareSerializer()
|
||||
protocols = serializers.SerializerMethodField()
|
||||
services = ServicesSerializer(default={})
|
||||
openRegistrations = serializers.SerializerMethodField()
|
||||
usage = serializers.SerializerMethodField()
|
||||
metadata = serializers.SerializerMethodField()
|
||||
|
||||
def get_version(self, obj) -> str:
|
||||
return "2.0"
|
||||
|
||||
def get_protocols(self, obj) -> list:
|
||||
return ["activitypub"]
|
||||
|
||||
def get_services(self, obj) -> object:
|
||||
return {"inbound": [], "outbound": []}
|
||||
|
||||
def get_openRegistrations(self, obj) -> bool:
|
||||
return obj["preferences"]["users__registration_enabled"]
|
||||
|
||||
@extend_schema_field(UsageSerializer)
|
||||
def get_usage(self, obj):
|
||||
usage = None
|
||||
if obj["preferences"]["instance__nodeinfo_stats_enabled"]:
|
||||
usage = obj["stats"]
|
||||
else:
|
||||
usage = {"users": {"total": 0, "activeMonth": 0, "activeHalfyear": 0}}
|
||||
return UsageSerializer(usage).data
|
||||
|
||||
@extend_schema_field(Metadata20Serializer)
|
||||
def get_metadata(self, obj):
|
||||
return Metadata20Serializer(obj).data
|
||||
|
||||
|
||||
class NodeInfo21Serializer(NodeInfo20Serializer):
|
||||
version = serializers.SerializerMethodField()
|
||||
software = SoftwareSerializer_v2()
|
||||
|
||||
def get_version(self, obj) -> str:
|
||||
return "2.1"
|
||||
|
||||
@extend_schema_field(UsageSerializer)
|
||||
def get_usage(self, obj):
|
||||
usage = None
|
||||
if obj["preferences"]["instance__nodeinfo_stats_enabled"]:
|
||||
usage = obj["stats"]
|
||||
usage["localPosts"] = 0
|
||||
usage["localComments"] = 0
|
||||
else:
|
||||
usage = {
|
||||
"users": {"total": 0, "activeMonth": 0, "activeHalfyear": 0},
|
||||
"localPosts": 0,
|
||||
"localComments": 0,
|
||||
}
|
||||
return UsageSerializer(usage).data
|
||||
|
||||
@extend_schema_field(Metadata21Serializer)
|
||||
def get_metadata(self, obj):
|
||||
return Metadata21Serializer(obj).data
|
||||
|
||||
|
||||
class SpaManifestIconSerializer(serializers.Serializer):
|
||||
src = serializers.CharField()
|
||||
sizes = serializers.CharField()
|
||||
type = serializers.CharField()
|
||||
|
||||
|
||||
class SpaManifestRelatedApplicationsSerializer(serializers.Serializer):
|
||||
platform = serializers.CharField()
|
||||
url = serializers.URLField()
|
||||
id = serializers.CharField()
|
||||
|
||||
|
||||
class SpaManifestShortcutSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
url = serializers.CharField()
|
||||
icons = SpaManifestIconSerializer(many=True, required=False)
|
||||
|
||||
|
||||
class SpaManifestSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(default="FunQuail")
|
||||
short_name = serializers.CharField(default="FunQuail")
|
||||
display = serializers.CharField(required=False)
|
||||
background_color = serializers.CharField(required=False)
|
||||
lang = serializers.CharField(required=False)
|
||||
categories = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
description = serializers.CharField(required=False)
|
||||
icons = SpaManifestIconSerializer(many=True, required=False)
|
||||
start_url = serializers.CharField(required=False)
|
||||
prefer_related_applications = serializers.BooleanField(required=False)
|
||||
related_applications = SpaManifestRelatedApplicationsSerializer(
|
||||
many=True, required=False
|
||||
)
|
||||
shortcuts = SpaManifestShortcutSerializer(many=True, required=False)
|
||||
99
api/funquail_api/instance/stats.py
Normal file
99
api/funquail_api/instance/stats.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import datetime
|
||||
|
||||
from django.db.models import Count, F, Sum
|
||||
from django.utils import timezone
|
||||
|
||||
from funkwhale_api.favorites.models import TrackFavorite
|
||||
from funkwhale_api.history.models import Listening
|
||||
from funkwhale_api.music import models
|
||||
from funkwhale_api.users.models import User
|
||||
|
||||
|
||||
def get():
|
||||
return {
|
||||
"users": get_users(),
|
||||
"tracks": get_tracks(),
|
||||
"albums": get_albums(),
|
||||
"artists": get_artists(),
|
||||
"track_favorites": get_track_favorites(),
|
||||
"listenings": get_listenings(),
|
||||
"downloads": get_downloads(),
|
||||
"music_duration": get_music_duration(),
|
||||
}
|
||||
|
||||
|
||||
def get_content():
|
||||
return {
|
||||
"local": {
|
||||
"artists": get_artists(),
|
||||
"releases": get_albums(),
|
||||
"recordings": get_tracks(),
|
||||
"hoursOfContent": get_music_duration(),
|
||||
},
|
||||
"topMusicCategories": get_top_music_categories(),
|
||||
"topPodcastCategories": get_top_podcast_categories(),
|
||||
}
|
||||
|
||||
|
||||
def get_top_music_categories():
|
||||
return (
|
||||
models.Track.objects.filter(artist__content_category="music")
|
||||
.exclude(tagged_items__tag_id=None)
|
||||
.values(name=F("tagged_items__tag__name"))
|
||||
.annotate(count=Count("name"))
|
||||
.order_by("-count")[:3]
|
||||
)
|
||||
|
||||
|
||||
def get_top_podcast_categories():
|
||||
return (
|
||||
models.Track.objects.filter(artist__content_category="podcast")
|
||||
.exclude(tagged_items__tag_id=None)
|
||||
.values(name=F("tagged_items__tag__name"))
|
||||
.annotate(count=Count("name"))
|
||||
.order_by("-count")[:3]
|
||||
)
|
||||
|
||||
|
||||
def get_users():
|
||||
qs = User.objects.filter(is_active=True)
|
||||
now = timezone.now()
|
||||
active_month = now - datetime.timedelta(days=30)
|
||||
active_halfyear = now - datetime.timedelta(days=30 * 6)
|
||||
return {
|
||||
"total": qs.count(),
|
||||
"active_month": qs.filter(last_activity__gte=active_month).count(),
|
||||
"active_halfyear": qs.filter(last_activity__gte=active_halfyear).count(),
|
||||
}
|
||||
return User.objects.count()
|
||||
|
||||
|
||||
def get_listenings():
|
||||
return Listening.objects.count()
|
||||
|
||||
|
||||
def get_track_favorites():
|
||||
return TrackFavorite.objects.count()
|
||||
|
||||
|
||||
def get_tracks():
|
||||
return models.Track.objects.local().count()
|
||||
|
||||
|
||||
def get_albums():
|
||||
return models.Album.objects.local().count()
|
||||
|
||||
|
||||
def get_artists():
|
||||
return models.Artist.objects.local().count()
|
||||
|
||||
|
||||
def get_downloads():
|
||||
return models.Track.objects.aggregate(d=Sum("downloads_count"))["d"] or 0
|
||||
|
||||
|
||||
def get_music_duration():
|
||||
seconds = models.Upload.objects.aggregate(d=Sum("duration"))["d"]
|
||||
if seconds:
|
||||
return seconds / 3600
|
||||
return 0
|
||||
14
api/funquail_api/instance/urls.py
Normal file
14
api/funquail_api/instance/urls.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from funkwhale_api.common import routers
|
||||
|
||||
from . import views
|
||||
|
||||
admin_router = routers.OptionalSlashRouter()
|
||||
admin_router.register(r"admin/settings", views.AdminSettings, "admin-settings")
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^nodeinfo/2.0/?$", views.NodeInfo20.as_view(), name="nodeinfo-2.0"),
|
||||
url(r"^settings/?$", views.InstanceSettings.as_view(), name="settings"),
|
||||
url(r"^spa-manifest.json", views.SpaManifest.as_view(), name="spa-manifest"),
|
||||
] + admin_router.urls
|
||||
7
api/funquail_api/instance/urls_v2.py
Normal file
7
api/funquail_api/instance/urls_v2.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^nodeinfo/2.1/?$", views.NodeInfo21.as_view(), name="nodeinfo-2.1"),
|
||||
]
|
||||
203
api/funquail_api/instance/views.py
Normal file
203
api/funquail_api/instance/views.py
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from cache_memoize import cache_memoize
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from dynamic_preferences.api import viewsets as preferences_viewsets
|
||||
from dynamic_preferences.api.serializers import GlobalPreferenceSerializer
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
from rest_framework import generics, views
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.response import Response
|
||||
|
||||
from funkwhale_api import __version__ as funkwhale_version
|
||||
from funkwhale_api.common import preferences
|
||||
from funkwhale_api.common.renderers import ActivityStreamRenderer
|
||||
from funkwhale_api.federation.actors import get_service_actor
|
||||
from funkwhale_api.federation.models import Domain
|
||||
from funkwhale_api.moderation.models import REPORT_TYPES
|
||||
from funkwhale_api.music.utils import SUPPORTED_EXTENSIONS
|
||||
from funkwhale_api.users.oauth import permissions as oauth_permissions
|
||||
|
||||
from . import serializers, stats
|
||||
|
||||
NODEINFO_2_CONTENT_TYPE = "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8" # noqa
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminSettings(preferences_viewsets.GlobalPreferencesViewSet):
|
||||
pagination_class = None
|
||||
permission_classes = [oauth_permissions.ScopePermission]
|
||||
required_scope = "instance:settings"
|
||||
|
||||
|
||||
class InstanceSettings(generics.GenericAPIView):
|
||||
permission_classes = []
|
||||
authentication_classes = []
|
||||
serializer_class = GlobalPreferenceSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
manager = global_preferences_registry.manager()
|
||||
manager.all()
|
||||
all_preferences = manager.model.objects.all().order_by("section", "name")
|
||||
api_preferences = [
|
||||
p for p in all_preferences if getattr(p.preference, "show_in_api", False)
|
||||
]
|
||||
return api_preferences
|
||||
|
||||
@extend_schema(operation_id="get_instance_settings")
|
||||
def get(self, request):
|
||||
queryset = self.get_queryset()
|
||||
data = GlobalPreferenceSerializer(queryset, many=True).data
|
||||
return Response(data, status=200)
|
||||
|
||||
|
||||
@method_decorator(ensure_csrf_cookie, name="dispatch")
|
||||
class NodeInfo20(views.APIView):
|
||||
permission_classes = []
|
||||
authentication_classes = []
|
||||
serializer_class = serializers.NodeInfo20Serializer
|
||||
renderer_classes = (JSONRenderer,)
|
||||
|
||||
@extend_schema(
|
||||
responses=serializers.NodeInfo20Serializer, operation_id="getNodeInfo20"
|
||||
)
|
||||
def get(self, request):
|
||||
pref = preferences.all()
|
||||
if (
|
||||
pref["moderation__allow_list_public"]
|
||||
and pref["moderation__allow_list_enabled"]
|
||||
):
|
||||
allowed_domains = list(
|
||||
Domain.objects.filter(allowed=True)
|
||||
.order_by("name")
|
||||
.values_list("name", flat=True)
|
||||
)
|
||||
else:
|
||||
allowed_domains = None
|
||||
|
||||
data = {
|
||||
"software": {"version": funkwhale_version},
|
||||
"services": {"inbound": ["atom1.0"], "outbound": ["atom1.0"]},
|
||||
"preferences": pref,
|
||||
"stats": cache_memoize(600, prefix="memoize:instance:stats")(stats.get)()
|
||||
if pref["instance__nodeinfo_stats_enabled"]
|
||||
else None,
|
||||
"actorId": get_service_actor().fid,
|
||||
"supportedUploadExtensions": SUPPORTED_EXTENSIONS,
|
||||
"allowed_domains": allowed_domains,
|
||||
"report_types": [
|
||||
{
|
||||
"type": t,
|
||||
"label": l,
|
||||
"anonymous": t
|
||||
in pref.get("moderation__unauthenticated_report_types"),
|
||||
}
|
||||
for t, l in REPORT_TYPES
|
||||
],
|
||||
"endpoints": {},
|
||||
}
|
||||
|
||||
if not pref.get("common__api_authentication_required"):
|
||||
if pref.get("instance__nodeinfo_stats_enabled"):
|
||||
data["endpoints"]["knownNodes"] = reverse(
|
||||
"api:v1:federation:domains-list"
|
||||
)
|
||||
if pref.get("federation__public_index"):
|
||||
data["endpoints"]["libraries"] = reverse(
|
||||
"federation:index:index-libraries"
|
||||
)
|
||||
data["endpoints"]["channels"] = reverse(
|
||||
"federation:index:index-channels"
|
||||
)
|
||||
serializer = self.serializer_class(data)
|
||||
return Response(
|
||||
serializer.data, status=200, content_type=NODEINFO_2_CONTENT_TYPE
|
||||
)
|
||||
|
||||
|
||||
class NodeInfo21(NodeInfo20):
|
||||
serializer_class = serializers.NodeInfo21Serializer
|
||||
|
||||
@extend_schema(
|
||||
responses=serializers.NodeInfo20Serializer, operation_id="getNodeInfo20"
|
||||
)
|
||||
def get(self, request):
|
||||
pref = preferences.all()
|
||||
if (
|
||||
pref["moderation__allow_list_public"]
|
||||
and pref["moderation__allow_list_enabled"]
|
||||
):
|
||||
allowed_domains = list(
|
||||
Domain.objects.filter(allowed=True)
|
||||
.order_by("name")
|
||||
.values_list("name", flat=True)
|
||||
)
|
||||
else:
|
||||
allowed_domains = None
|
||||
|
||||
data = {
|
||||
"software": {"version": funkwhale_version},
|
||||
"services": {"inbound": ["atom1.0"], "outbound": ["atom1.0"]},
|
||||
"preferences": pref,
|
||||
"stats": cache_memoize(600, prefix="memoize:instance:stats")(stats.get)()
|
||||
if pref["instance__nodeinfo_stats_enabled"]
|
||||
else None,
|
||||
"actorId": get_service_actor().fid,
|
||||
"supportedUploadExtensions": SUPPORTED_EXTENSIONS,
|
||||
"allowed_domains": allowed_domains,
|
||||
"languages": pref.get("moderation__languages"),
|
||||
"location": pref.get("instance__location"),
|
||||
"content": cache_memoize(600, prefix="memoize:instance:content")(
|
||||
stats.get_content
|
||||
)()
|
||||
if pref["instance__nodeinfo_stats_enabled"]
|
||||
else None,
|
||||
"features": [
|
||||
"channels",
|
||||
"podcasts",
|
||||
],
|
||||
}
|
||||
|
||||
if not pref.get("common__api_authentication_required"):
|
||||
data["features"].append("anonymousCanListen")
|
||||
|
||||
if pref.get("federation__enabled"):
|
||||
data["features"].append("federation")
|
||||
|
||||
serializer = self.serializer_class(data)
|
||||
return Response(
|
||||
serializer.data, status=200, content_type=NODEINFO_2_CONTENT_TYPE
|
||||
)
|
||||
|
||||
|
||||
PWA_MANIFEST_PATH = Path(__file__).parent / "pwa-manifest.json"
|
||||
PWA_MANIFEST: dict = json.loads(PWA_MANIFEST_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class SpaManifest(generics.GenericAPIView):
|
||||
permission_classes = []
|
||||
authentication_classes = []
|
||||
serializer_class = serializers.SpaManifestSerializer
|
||||
renderer_classes = [ActivityStreamRenderer]
|
||||
|
||||
@extend_schema(operation_id="get_spa_manifest")
|
||||
def get(self, request):
|
||||
manifest = PWA_MANIFEST.copy()
|
||||
instance_name = preferences.get("instance__name")
|
||||
if instance_name:
|
||||
manifest["short_name"] = instance_name
|
||||
manifest["name"] = instance_name
|
||||
instance_description = preferences.get("instance__short_description")
|
||||
if instance_description:
|
||||
manifest["description"] = instance_description
|
||||
serializer = self.get_serializer(manifest)
|
||||
return Response(
|
||||
serializer.data, status=200, content_type="application/manifest+json"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue