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
  • 278-search-browse
  • 303-json-ld
  • 316-ultrasonic
  • 334-don-t-display-an-empty-page-browser
  • 463-user-libraries
  • ButterflyOfFire/funkwhale-patch-1
  • avatar-everywhere
  • build-docker-unprivileged
  • develop
  • master
  • playlist-component
  • 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.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 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
50 results
Show changes
Showing
with 2041 additions and 369 deletions
#!/bin/bash
set -e
exec "$@"
#!/bin/bash
set -e
# This entrypoint is used to play nicely with the current cookiecutter configuration.
# Since docker-compose relies heavily on environment variables itself for configuration, we'd have to define multiple
# environment variables just to support cookiecutter out of the box. That makes no sense, so this little entrypoint
# does all this for us.
export CACHE_URL=${CACHE_URL:="redis://redis:6379/0"}
if [ -z "$DATABASE_URL" ]; then
# the official postgres image uses 'postgres' as default user if not set explictly.
if [ -z "$POSTGRES_ENV_POSTGRES_USER" ]; then
export POSTGRES_ENV_POSTGRES_USER=postgres
fi
export DATABASE_URL=postgres://$POSTGRES_ENV_POSTGRES_USER:$POSTGRES_ENV_POSTGRES_PASSWORD@postgres:5432/$POSTGRES_ENV_POSTGRES_USER
fi
if [ -z "$CELERY_BROKER_URL" ]; then
export CELERY_BROKER_URL=$CACHE_URL
fi
# we copy the frontend files, if any so we can serve them from the outside
if [ -d "frontend" ]; then
mkdir -p /frontend
cp -r frontend/* /frontend/
fi
exec "$@"
import os import os
import django os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
os.environ.setdefault("ASGI_THREADS", "5")
import django # noqa
django.setup() django.setup()
from .routing import application # noqa from .routing import application # noqa
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
import copy
import logging
import os
import subprocess
import sys
import persisting_theory
from django.core.cache import cache
from django.db.models import Q
from rest_framework import serializers
logger = logging.getLogger("plugins")
class Startup(persisting_theory.Registry):
look_into = "persisting_theory"
class Ready(persisting_theory.Registry):
look_into = "persisting_theory"
startup = Startup()
ready = Ready()
_plugins = {}
_filters = {}
_hooks = {}
class PluginCache:
def __init__(self, prefix):
self.prefix = prefix
def get(self, key, default=None):
key = ":".join([self.prefix, key])
return cache.get(key, default)
def set(self, key, value, duration=None):
key = ":".join([self.prefix, key])
return cache.set(key, value, duration)
def get_plugin_config(
name,
user=False,
source=False,
registry=_plugins,
conf={},
settings={},
description=None,
version=None,
label=None,
homepage=None,
):
conf = {
"name": name,
"label": label or name,
"logger": logger,
# conf is for dynamic settings
"conf": conf,
# settings is for settings hardcoded in .env
"settings": settings,
"user": True if source else user,
# source plugins are plugins that provide audio content
"source": source,
"description": description,
"version": version,
"cache": PluginCache(name),
"homepage": homepage,
}
registry[name] = conf
return conf
def load_settings(name, settings):
from django.conf import settings as django_settings
mapping = {
"boolean": django_settings.ENV.bool,
"text": django_settings.ENV,
}
values = {}
prefix = f"FUNKWHALE_PLUGIN_{name.upper()}"
for s in settings:
key = "_".join([prefix, s["name"].upper()])
value = mapping[s["type"]](key, default=s.get("default", None))
values[s["name"]] = value
logger.debug("Plugin %s running with settings %s", name, values)
return values
def get_session():
from funkwhale_api.common import session
return session.get_session()
def register_filter(name, plugin_config, registry=_filters):
def decorator(func):
handlers = registry.setdefault(name, [])
def inner(*args, **kwargs):
plugin_config["logger"].debug("Calling filter for %s", name)
rval = func(*args, **kwargs)
return rval
handlers.append((plugin_config["name"], inner))
return inner
return decorator
def register_hook(name, plugin_config, registry=_hooks):
def decorator(func):
handlers = registry.setdefault(name, [])
def inner(*args, **kwargs):
plugin_config["logger"].debug("Calling hook for %s", name)
func(*args, **kwargs)
handlers.append((plugin_config["name"], inner))
return inner
return decorator
class Skip(Exception):
pass
def trigger_filter(name, value, enabled=False, **kwargs):
"""
Call filters registered for "name" with the given
args and kwargs.
Return the value (that could be modified by handlers)
"""
logger.debug("Calling handlers for filter %s", name)
registry = kwargs.pop("registry", _filters)
confs = kwargs.pop("confs", {})
for plugin_name, handler in registry.get(name, []):
if not enabled and confs.get(plugin_name, {}).get("enabled") is False:
continue
try:
value = handler(value, conf=confs.get(plugin_name, {}), **kwargs)
except Skip:
pass
except Exception as e:
logger.warn("Plugin %s errored during filter %s: %s", plugin_name, name, e)
return value
def trigger_hook(name, enabled=False, **kwargs):
"""
Call hooks registered for "name" with the given
args and kwargs.
Returns nothing
"""
logger.debug("Calling handlers for hook %s", name)
registry = kwargs.pop("registry", _hooks)
confs = kwargs.pop("confs", {})
for plugin_name, handler in registry.get(name, []):
if not enabled and confs.get(plugin_name, {}).get("enabled") is False:
continue
try:
handler(conf=confs.get(plugin_name, {}).get("conf"), **kwargs)
except Skip:
pass
except Exception as e:
logger.warn("Plugin %s errored during hook %s: %s", plugin_name, name, e)
def set_conf(name, conf, user=None, registry=_plugins):
from funkwhale_api.common import models
if not registry[name]["conf"] and not registry[name]["source"]:
return
conf_serializer = get_serializer_from_conf_template(
registry[name]["conf"],
user=user,
source=registry[name]["source"],
)(data=conf)
conf_serializer.is_valid(raise_exception=True)
if "library" in conf_serializer.validated_data:
conf_serializer.validated_data["library"] = str(
conf_serializer.validated_data["library"]
)
conf, _ = models.PluginConfiguration.objects.update_or_create(
user=user, code=name, defaults={"conf": conf_serializer.validated_data}
)
def get_confs(user=None):
from funkwhale_api.common import models
qs = models.PluginConfiguration.objects.filter(code__in=list(_plugins.keys()))
if user:
qs = qs.filter(Q(user=None) | Q(user=user))
else:
qs = qs.filter(user=None)
confs = {
v["code"]: {"conf": v["conf"], "enabled": v["enabled"]}
for v in qs.values("code", "conf", "enabled")
}
for p, v in _plugins.items():
if p not in confs:
confs[p] = {"conf": None, "enabled": False}
return confs
def get_conf(plugin, user=None):
return get_confs(user=user)[plugin]
def enable_conf(code, value, user):
from funkwhale_api.common import models
models.PluginConfiguration.objects.update_or_create(
code=code, user=user, defaults={"enabled": value}
)
class LibraryField(serializers.UUIDField):
def __init__(self, *args, **kwargs):
self.actor = kwargs.pop("actor")
super().__init__(*args, **kwargs)
def to_internal_value(self, v):
v = super().to_internal_value(v)
if not self.actor.libraries.filter(uuid=v).first():
raise serializers.ValidationError("Invalid library id")
return v
def get_serializer_from_conf_template(conf, source=False, user=None):
conf = copy.deepcopy(conf)
validators = {f["name"]: f.pop("validator") for f in conf if "validator" in f}
mapping = {
"url": serializers.URLField,
"boolean": serializers.BooleanField,
"text": serializers.CharField,
"long_text": serializers.CharField,
"password": serializers.CharField,
"number": serializers.IntegerField,
}
for attr in ["label", "help"]:
for c in conf:
c.pop(attr, None)
class Serializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_conf in conf:
field_kwargs = copy.copy(field_conf)
name = field_kwargs.pop("name")
self.fields[name] = mapping[field_kwargs.pop("type")](**field_kwargs)
if source:
self.fields["library"] = LibraryField(actor=user.actor)
for vname, v in validators.items():
setattr(Serializer, f"validate_{vname}", v)
return Serializer
def serialize_plugin(plugin_conf, confs):
return {
"name": plugin_conf["name"],
"label": plugin_conf["label"],
"description": plugin_conf.get("description") or None,
"user": plugin_conf.get("user", False),
"source": plugin_conf.get("source", False),
"conf": plugin_conf.get("conf", None),
"values": confs.get(plugin_conf["name"], {"conf"}).get("conf"),
"enabled": plugin_conf["name"] in confs
and confs[plugin_conf["name"]]["enabled"],
"homepage": plugin_conf["homepage"],
}
def install_dependencies(deps):
if not deps:
return
logger.info("Installing plugins dependencies %s", deps)
pip_path = os.path.join(os.path.dirname(sys.executable), "pip")
subprocess.check_call([pip_path, "install"] + deps)
def background_task(name):
from funkwhale_api.taskapp import celery
def decorator(func):
return celery.app.task(func, name=name)
return decorator
# HOOKS
TRIGGER_THIRD_PARTY_UPLOAD = "third_party_upload"
"""
Called when a track is being listened
"""
LISTENING_CREATED = "listening_created"
"""
Called when a track is being listened
"""
LISTENING_SYNC = "listening_sync"
"""
Called by the task manager to trigger listening sync
"""
FAVORITE_CREATED = "favorite_created"
"""
Called when a track is being favorited
"""
FAVORITE_DELETED = "favorite_deleted"
"""
Called when a favorited track is being unfavorited
"""
FAVORITE_SYNC = "favorite_sync"
"""
Called by the task manager to trigger favorite sync
"""
SCAN = "scan"
"""
"""
# FILTERS
PLUGINS_DEPENDENCIES = "plugins_dependencies"
"""
Called with an empty list, use this filter to append pip dependencies
to the list for installation.
"""
PLUGINS_APPS = "plugins_apps"
"""
Called with an empty list, use this filter to append apps to INSTALLED_APPS
"""
MIDDLEWARES_BEFORE = "middlewares_before"
"""
Called with an empty list, use this filter to prepend middlewares
to MIDDLEWARE
"""
MIDDLEWARES_AFTER = "middlewares_after"
"""
Called with an empty list, use this filter to append middlewares
to MIDDLEWARE
"""
URLS = "urls"
"""
Called with an empty list, use this filter to register new urls and views
"""
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url from django.core.asgi import get_asgi_application
from django.urls import re_path
from funkwhale_api.common.auth import TokenAuthMiddleware
from funkwhale_api.instance import consumers from funkwhale_api.instance import consumers
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {
# Empty for now (http->django views is added by default) # Empty for now (http->django views is added by default)
"websocket": TokenAuthMiddleware( "websocket": AuthMiddlewareStack(
URLRouter( URLRouter(
[url("^api/v1/instance/activity$", consumers.InstanceActivityConsumer)] [
re_path(
"^api/v1/activity$",
consumers.InstanceActivityConsumer.as_asgi(),
) )
]
) )
),
"http": get_asgi_application(),
} }
) )
from drf_spectacular.contrib.django_oauth_toolkit import OpenApiAuthenticationExtension
from drf_spectacular.plumbing import build_bearer_security_scheme_object
class CustomOAuthExt(OpenApiAuthenticationExtension):
target_class = "funkwhale_api.common.authentication.OAuth2Authentication"
name = "oauth2"
def get_security_definition(self, auto_schema):
from drf_spectacular.settings import spectacular_settings
from oauth2_provider.scopes import get_scopes_backend
flows = {}
for flow_type in spectacular_settings.OAUTH2_FLOWS:
flows[flow_type] = {}
if flow_type in ("implicit", "authorizationCode"):
flows[flow_type][
"authorizationUrl"
] = spectacular_settings.OAUTH2_AUTHORIZATION_URL
if flow_type in ("password", "clientCredentials", "authorizationCode"):
flows[flow_type]["tokenUrl"] = spectacular_settings.OAUTH2_TOKEN_URL
if spectacular_settings.OAUTH2_REFRESH_URL:
flows[flow_type]["refreshUrl"] = spectacular_settings.OAUTH2_REFRESH_URL
scope_backend = get_scopes_backend()
flows[flow_type]["scopes"] = scope_backend.get_all_scopes()
return {"type": "oauth2", "flows": flows}
class CustomApplicationTokenExt(OpenApiAuthenticationExtension):
target_class = "funkwhale_api.common.authentication.ApplicationTokenAuthentication"
name = "ApplicationToken"
def get_security_definition(self, auto_schema):
return build_bearer_security_scheme_object(
header_name="Authorization",
token_prefix="Bearer",
)
def custom_preprocessing_hook(endpoints):
filtered = []
# your modifications to the list of operations that are exposed in the schema
for path, path_regex, method, callback in endpoints:
if path.startswith("/api/v1/providers"):
continue
if path.startswith("/api/v1/users/users"):
continue
if path.startswith("/api/v1/oauth/authorize"):
continue
if path.startswith("/api/v1") or path.startswith("/api/v2"):
filtered.append((path, path_regex, method, callback))
return filtered
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*- import logging.config
""" import sys
Django settings for funkwhale_api project. import warnings
from collections import OrderedDict
For more information on this file, see
https://docs.djangoproject.com/en/dev/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/dev/ref/settings/
"""
from __future__ import absolute_import, unicode_literals
import datetime
from urllib.parse import urlparse, urlsplit from urllib.parse import urlparse, urlsplit
import environ import environ
from celery.schedules import crontab from celery.schedules import crontab
from funkwhale_api import __version__ logger = logging.getLogger("funkwhale_api.config")
ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /) ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /)
APPS_DIR = ROOT_DIR.path("funkwhale_api") APPS_DIR = ROOT_DIR.path("funkwhale_api")
env = environ.Env() env = environ.Env()
ENV = env
# If DEBUG is `true`, we automatically set the loglevel to "DEBUG"
# If DEBUG is `false`, we try to read the level from LOGLEVEL environment and default to "INFO"
LOGLEVEL = (
"DEBUG" if env.bool("DEBUG", False) else env("LOGLEVEL", default="info").upper()
)
"""
Default logging level for the Funkwhale processes.
.. note::
The `DEBUG` variable overrides the `LOGLEVEL` if it is set to `TRUE`.
The `LOGLEVEL` value only applies if `DEBUG` is `false` or not present.
Available levels:
- ``debug``
- ``info``
- ``warning``
- ``error``
- ``critical``
"""
IS_DOCKER_SETUP = env.bool("IS_DOCKER_SETUP", False)
if env("FUNKWHALE_SENTRY_DSN", default=None) is not None:
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from funkwhale_api import __version__ as version
sentry_sdk.init(
dsn=env("FUNKWHALE_SENTRY_DSN"),
integrations=[DjangoIntegration(), CeleryIntegration()],
traces_sample_rate=env("FUNKWHALE_SENTRY_SR", default=0.25),
send_default_pii=False,
environment="api",
debug=env.bool("DEBUG", False),
release=version,
)
sentry_sdk.set_tag("instance", env("FUNKWHALE_HOSTNAME"))
LOGGING_CONFIG = None
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"console": {"format": "%(asctime)s %(name)-12s %(levelname)-8s %(message)s"}
},
"handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "console"},
},
"loggers": {
"funkwhale_api": {
"level": LOGLEVEL,
"handlers": ["console"],
# required to avoid double logging with root logger
"propagate": False,
},
"plugins": {
"level": LOGLEVEL,
"handlers": ["console"],
# required to avoid double logging with root logger
"propagate": False,
},
"": {"level": "WARNING", "handlers": ["console"]},
},
}
)
ENV_FILE = env_file = env("ENV_FILE", default=None)
"""
Path to a .env file to load
"""
if env_file:
logger.info("Loading specified env file at %s", env_file)
# we have an explicitly specified env file
# so we try to load and it fail loudly if it does not exist
env.read_env(env_file)
else:
# we try to load from .env and config/.env
# but do not crash if those files don't exist
paths = [
# /srv/funwhale/api/.env
ROOT_DIR,
# /srv/funwhale/config/.env
((ROOT_DIR - 1) + "config"),
]
for path in paths:
try: try:
env.read_env(ROOT_DIR.file(".env")) env_path = path.file(".env")
except FileNotFoundError: except FileNotFoundError:
pass logger.debug("No env file found at %s/.env", path)
continue
env.read_env(env_path)
logger.info("Loaded env file at %s/.env", path)
break
FUNKWHALE_PLUGINS = env("FUNKWHALE_PLUGINS", default="")
FUNKWHALE_PLUGINS_PATH = env(
"FUNKWHALE_PLUGINS_PATH", default="/srv/funkwhale/plugins/"
)
"""
Path to a directory containing Funkwhale plugins.
These are imported at runtime.
"""
sys.path.append(FUNKWHALE_PLUGINS_PATH)
CORE_PLUGINS = [
"funkwhale_api.contrib.scrobbler",
"funkwhale_api.contrib.listenbrainz",
"funkwhale_api.contrib.maloja",
]
LOAD_CORE_PLUGINS = env.bool("FUNKWHALE_LOAD_CORE_PLUGINS", default=True)
PLUGINS = [p for p in env.list("FUNKWHALE_PLUGINS", default=[]) if p]
"""
List of Funkwhale plugins to load.
"""
if LOAD_CORE_PLUGINS:
PLUGINS = CORE_PLUGINS + PLUGINS
# Remove duplicates
PLUGINS = list(OrderedDict.fromkeys(PLUGINS))
if PLUGINS:
logger.info("Running with the following plugins enabled: %s", ", ".join(PLUGINS))
else:
logger.info("Running with no plugins")
from .. import plugins # noqa
plugins.startup.autodiscover([p + ".funkwhale_startup" for p in PLUGINS])
DEPENDENCIES = plugins.trigger_filter(plugins.PLUGINS_DEPENDENCIES, [], enabled=True)
plugins.install_dependencies(DEPENDENCIES)
FUNKWHALE_HOSTNAME = None FUNKWHALE_HOSTNAME = None
FUNKWHALE_HOSTNAME_SUFFIX = env("FUNKWHALE_HOSTNAME_SUFFIX", default=None) FUNKWHALE_HOSTNAME_SUFFIX = env("FUNKWHALE_HOSTNAME_SUFFIX", default=None)
FUNKWHALE_HOSTNAME_PREFIX = env("FUNKWHALE_HOSTNAME_PREFIX", default=None) FUNKWHALE_HOSTNAME_PREFIX = env("FUNKWHALE_HOSTNAME_PREFIX", default=None)
...@@ -39,33 +162,84 @@ if FUNKWHALE_HOSTNAME_PREFIX and FUNKWHALE_HOSTNAME_SUFFIX: ...@@ -39,33 +162,84 @@ if FUNKWHALE_HOSTNAME_PREFIX and FUNKWHALE_HOSTNAME_SUFFIX:
else: else:
try: try:
FUNKWHALE_HOSTNAME = env("FUNKWHALE_HOSTNAME") FUNKWHALE_HOSTNAME = env("FUNKWHALE_HOSTNAME")
"""
Hostname of your Funkwhale pod, e.g. ``mypod.audio``.
"""
FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https") FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
"""
Protocol end users will use to access your pod, either
``http`` or ``https``.
"""
except Exception: except Exception:
FUNKWHALE_URL = env("FUNKWHALE_URL") FUNKWHALE_URL = env("FUNKWHALE_URL")
_parsed = urlsplit(FUNKWHALE_URL) _parsed = urlsplit(FUNKWHALE_URL)
FUNKWHALE_HOSTNAME = _parsed.netloc FUNKWHALE_HOSTNAME = _parsed.netloc
FUNKWHALE_PROTOCOL = _parsed.scheme FUNKWHALE_PROTOCOL = _parsed.scheme
FUNKWHALE_URL = "{}://{}".format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME) FUNKWHALE_PROTOCOL = FUNKWHALE_PROTOCOL.lower()
FUNKWHALE_HOSTNAME = FUNKWHALE_HOSTNAME.lower()
FUNKWHALE_URL = f"{FUNKWHALE_PROTOCOL}://{FUNKWHALE_HOSTNAME}"
FUNKWHALE_SPA_HTML_ROOT = env("FUNKWHALE_SPA_HTML_ROOT", default=FUNKWHALE_URL)
"""
URL or path to the Web Application files.
Funkwhale needs access to it so that it can inject <meta> tags relevant
to the given page (e.g page title, cover, etc.).
If a URL is specified, the index.html file will be fetched through HTTP.
If a path is provided,
it will be accessed from disk.
Use something like ``/srv/funkwhale/front/dist/`` if the web processes shows
request errors related to this.
"""
# XXX: deprecated, see #186 FUNKWHALE_SPA_HTML_CACHE_DURATION = env.int(
FEDERATION_ENABLED = env.bool("FEDERATION_ENABLED", default=True) "FUNKWHALE_SPA_HTML_CACHE_DURATION", default=60 * 15
FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME) )
# XXX: deprecated, see #186 FUNKWHALE_EMBED_URL = env("FUNKWHALE_EMBED_URL", default=FUNKWHALE_URL + "/embed.html")
FEDERATION_COLLECTION_PAGE_SIZE = env.int("FEDERATION_COLLECTION_PAGE_SIZE", default=50) FUNKWHALE_SPA_REWRITE_MANIFEST = env.bool(
# XXX: deprecated, see #186 "FUNKWHALE_SPA_REWRITE_MANIFEST", default=True
FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool(
"FEDERATION_MUSIC_NEEDS_APPROVAL", default=True
) )
# XXX: deprecated, see #186 FUNKWHALE_SPA_REWRITE_MANIFEST_URL = env.bool(
FEDERATION_ACTOR_FETCH_DELAY = env.int("FEDERATION_ACTOR_FETCH_DELAY", default=60 * 12) "FUNKWHALE_SPA_REWRITE_MANIFEST_URL", default=None
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS") )
APP_NAME = "Funkwhale"
FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME).lower()
FEDERATION_SERVICE_ACTOR_USERNAME = env(
"FEDERATION_SERVICE_ACTOR_USERNAME", default="service"
)
# How many pages to fetch when crawling outboxes and third-party collections
FEDERATION_COLLECTION_MAX_PAGES = env.int("FEDERATION_COLLECTION_MAX_PAGES", default=5)
"""
Number of existing pages of content to fetch when discovering/refreshing an
actor or channel.
More pages means more content will be loaded, but will require more resources.
"""
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=[]) + [FUNKWHALE_HOSTNAME]
"""
List of allowed hostnames for which the Funkwhale server will answer.
"""
CSRF_TRUSTED_ORIGINS = [
urlparse("//" + o, FUNKWHALE_PROTOCOL).geturl() for o in ALLOWED_HOSTS
]
"""
List of origins that are trusted for unsafe requests
We simply consider all allowed hosts to be trusted origins
See DJANGO_ALLOWED_HOSTS in .env.example for details
See https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
"""
# APP CONFIGURATION # APP CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DJANGO_APPS = ( DJANGO_APPS = (
"channels", "channels",
"daphne",
# Default Django apps: # Default Django apps:
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
...@@ -85,82 +259,93 @@ THIRD_PARTY_APPS = ( ...@@ -85,82 +259,93 @@ THIRD_PARTY_APPS = (
"allauth.account", # registration "allauth.account", # registration
"allauth.socialaccount", # registration "allauth.socialaccount", # registration
"corsheaders", "corsheaders",
"oauth2_provider",
"rest_framework", "rest_framework",
"rest_framework.authtoken", "rest_framework.authtoken",
"taggit", "dj_rest_auth",
"rest_auth", "dj_rest_auth.registration",
"rest_auth.registration",
"dynamic_preferences", "dynamic_preferences",
"django_filters", "django_filters",
"cacheops",
"django_cleanup", "django_cleanup",
"versatileimagefield",
) )
# Sentry
RAVEN_ENABLED = env.bool("RAVEN_ENABLED", default=False)
RAVEN_DSN = env("RAVEN_DSN", default="")
if RAVEN_ENABLED:
RAVEN_CONFIG = {
"dsn": RAVEN_DSN,
# If you are using git, you can also automatically configure the
# release based on the git info.
"release": __version__,
}
THIRD_PARTY_APPS += ("raven.contrib.django.raven_compat",)
# Apps specific for this project go here. # Apps specific for this project go here.
LOCAL_APPS = ( LOCAL_APPS = (
"funkwhale_api.common", "funkwhale_api.common.apps.CommonConfig",
"funkwhale_api.activity.apps.ActivityConfig", "funkwhale_api.activity.apps.ActivityConfig",
"funkwhale_api.users", # custom users app "funkwhale_api.users", # custom users app
"funkwhale_api.users.oauth",
# Your stuff: custom apps go here # Your stuff: custom apps go here
"funkwhale_api.instance", "funkwhale_api.instance",
"funkwhale_api.audio",
"funkwhale_api.contrib.listenbrainz",
"funkwhale_api.music", "funkwhale_api.music",
"funkwhale_api.requests", "funkwhale_api.requests",
"funkwhale_api.favorites", "funkwhale_api.favorites",
"funkwhale_api.federation", "funkwhale_api.federation",
"funkwhale_api.moderation.apps.ModerationConfig",
"funkwhale_api.radios", "funkwhale_api.radios",
"funkwhale_api.history", "funkwhale_api.history",
"funkwhale_api.playlists", "funkwhale_api.playlists",
"funkwhale_api.providers.audiofile",
"funkwhale_api.providers.youtube",
"funkwhale_api.providers.acoustid",
"funkwhale_api.subsonic", "funkwhale_api.subsonic",
"funkwhale_api.tags",
"funkwhale_api.typesense",
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS ADDITIONAL_APPS = env.list("ADDITIONAL_APPS", default=[])
"""
List of Django apps to load in addition to Funkwhale plugins and apps.
"""
INSTALLED_APPS = (
LOCAL_APPS
+ DJANGO_APPS
+ THIRD_PARTY_APPS
+ tuple(ADDITIONAL_APPS)
+ tuple(plugins.trigger_filter(plugins.PLUGINS_APPS, [], enabled=True))
)
# MIDDLEWARE CONFIGURATION # MIDDLEWARE CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
ADDITIONAL_MIDDLEWARES_BEFORE = env.list("ADDITIONAL_MIDDLEWARES_BEFORE", default=[])
MIDDLEWARE = ( MIDDLEWARE = (
# Make sure djangosecure.middleware.SecurityMiddleware is listed first tuple(plugins.trigger_filter(plugins.MIDDLEWARES_BEFORE, [], enabled=True))
"django.contrib.sessions.middleware.SessionMiddleware", + tuple(ADDITIONAL_MIDDLEWARES_BEFORE)
+ (
"allauth.account.middleware.AccountMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware", "corsheaders.middleware.CorsMiddleware",
# needs to be before SPA middleware
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
# /end
"funkwhale_api.common.middleware.SPAFallbackMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"funkwhale_api.users.middleware.RecordActivityMiddleware", "funkwhale_api.users.middleware.RecordActivityMiddleware",
"funkwhale_api.common.middleware.ThrottleStatusMiddleware",
)
+ tuple(plugins.trigger_filter(plugins.MIDDLEWARES_AFTER, [], enabled=True))
) )
# MIGRATIONS CONFIGURATION
# ------------------------------------------------------------------------------
MIGRATION_MODULES = {"sites": "funkwhale_api.contrib.sites.migrations"}
# DEBUG # DEBUG
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug # See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = env.bool("DJANGO_DEBUG", False) DJANGO_DEBUG = DEBUG = env.bool("DJANGO_DEBUG", False)
"""
Whether to enable debugging info and pages.
Never enable this on a production server, as it can leak very sensitive
information.
"""
# FIXTURE CONFIGURATION # FIXTURE CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS
FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),) FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
# EMAIL CONFIGURATION # EMAIL CONFIGURATION
...@@ -169,32 +354,133 @@ FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),) ...@@ -169,32 +354,133 @@ FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
# EMAIL # EMAIL
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DEFAULT_FROM_EMAIL = env( DEFAULT_FROM_EMAIL = env(
"DEFAULT_FROM_EMAIL", default="Funkwhale <noreply@{}>".format(FUNKWHALE_HOSTNAME) "DEFAULT_FROM_EMAIL", default=f"Funkwhale <noreply@{FUNKWHALE_HOSTNAME}>"
) )
"""
The name and email address used to send system emails.
Defaults to ``Funkwhale <noreply@yourdomain>``.
Available formats:
- ``Name <email address>``
- ``<Email address>``
"""
EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Funkwhale] ") EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Funkwhale] ")
"""
Subject prefix for system emails.
"""
SERVER_EMAIL = env("SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) SERVER_EMAIL = env("SERVER_EMAIL", default=DEFAULT_FROM_EMAIL)
EMAIL_CONFIG = env.email_url("EMAIL_CONFIG", default="consolemail://") EMAIL_CONFIG = env.email_url("EMAIL_CONFIG", default="consolemail://")
"""
SMTP configuration for sending emails. Possible values:
- ``EMAIL_CONFIG=consolemail://``: output emails to console (the default)
- ``EMAIL_CONFIG=dummymail://``: disable email sending completely
On a production instance, you'll usually want to use an external SMTP server:
- ``EMAIL_CONFIG=smtp://user:password@youremail.host:25``
- ``EMAIL_CONFIG=smtp+ssl://user:password@youremail.host:465``
- ``EMAIL_CONFIG=smtp+tls://user:password@youremail.host:587``
"""
vars().update(EMAIL_CONFIG) vars().update(EMAIL_CONFIG)
# DATABASE CONFIGURATION # DATABASE CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
# The `_database_url_docker` variable will only by used as default for DATABASE_URL
# in the context of a docker deployment.
_database_url_docker = None
if IS_DOCKER_SETUP and env.str("DATABASE_URL", None) is None:
warnings.warn(
DeprecationWarning(
"the automatically generated 'DATABASE_URL' configuration in the docker "
"setup is deprecated, please configure either the 'DATABASE_URL' "
"environment variable or the 'DATABASE_HOST', 'DATABASE_USER' and "
"'DATABASE_PASSWORD' environment variables instead"
)
)
_DOCKER_DATABASE_HOST = "postgres"
_DOCKER_DATABASE_PORT = 5432
_DOCKER_DATABASE_USER = env.str("POSTGRES_ENV_POSTGRES_USER", "postgres")
_DOCKER_DATABASE_PASSWORD = env.str("POSTGRES_ENV_POSTGRES_PASSWORD", "")
_DOCKER_DATABASE_NAME = _DOCKER_DATABASE_USER
_database_url_docker = (
f"postgres:"
f"//{_DOCKER_DATABASE_USER}:{_DOCKER_DATABASE_PASSWORD}"
f"@{_DOCKER_DATABASE_HOST}:{_DOCKER_DATABASE_PORT}"
f"/{_DOCKER_DATABASE_NAME}"
)
DATABASE_HOST = env.str("DATABASE_HOST", "localhost")
"""
The hostname of the PostgreSQL server. Defaults to ``localhost``.
"""
DATABASE_PORT = env.int("DATABASE_PORT", 5432)
"""
The port of the PostgreSQL server. Defaults to ``5432``.
"""
DATABASE_USER = env.str("DATABASE_USER", "funkwhale")
"""
The name of the PostgreSQL user. Defaults to ``funkwhale``.
"""
DATABASE_PASSWORD = env.str("DATABASE_PASSWORD", "funkwhale")
"""
The password of the PostgreSQL user. Defaults to ``funkwhale``.
"""
DATABASE_NAME = env.str("DATABASE_NAME", "funkwhale")
"""
The name of the PostgreSQL database. Defaults to ``funkwhale``.
"""
DATABASE_URL = env.db(
"DATABASE_URL",
_database_url_docker # This is only set in the context of a docker deployment.
or (
f"postgres:"
f"//{DATABASE_USER}:{DATABASE_PASSWORD}"
f"@{DATABASE_HOST}:{DATABASE_PORT}"
f"/{DATABASE_NAME}"
),
)
"""
The URL used to connect to the PostgreSQL database. Defaults to an auto generated url
build using the `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USER`, `DATABASE_PASSWORD`
and `DATABASE_NAME` variables.
Examples:
- ``postgresql://funkwhale@:5432/funkwhale``
- ``postgresql://<user>:<password>@<host>:<port>/<database>``
- ``postgresql://funkwhale:passw0rd@localhost:5432/funkwhale_database``
"""
DATABASES = { DATABASES = {
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
"default": env.db("DATABASE_URL") "default": DATABASE_URL
} }
DATABASES["default"]["ATOMIC_REQUESTS"] = True DATABASES["default"]["ATOMIC_REQUESTS"] = True
# DB_CONN_MAX_AGE = DATABASES["default"]["CONN_MAX_AGE"] = env(
# DATABASES = { "DB_CONN_MAX_AGE", default=60 * 5
# 'default': { )
# 'ENGINE': 'django.db.backends.sqlite3', """
# 'NAME': 'db.sqlite3', The maximum time in seconds before database connections close.
# } """
# } MIGRATION_MODULES = {
# see https://github.com/jazzband/django-oauth-toolkit/issues/634
# swappable models are badly designed in oauth2_provider
# ignore migrations and provide our own models.
"oauth2_provider": None,
"sites": "funkwhale_api.contrib.sites.migrations",
}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# see https://docs.djangoproject.com/en/4.0/releases/3.2/
# GENERAL CONFIGURATION # GENERAL CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Local time zone for this installation. Choices can be found here: # Local time zone for this installation. Choices can be found here:
...@@ -223,20 +509,25 @@ USE_TZ = True ...@@ -223,20 +509,25 @@ USE_TZ = True
# See: https://docs.djangoproject.com/en/dev/ref/settings/#templates # See: https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES = [ TEMPLATES = [
{ {
# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
"DIRS": [str(APPS_DIR.path("templates"))], "DIRS": [str(APPS_DIR.path("templates"))],
"OPTIONS": { "OPTIONS": {
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#template-debug
"debug": DEBUG, "debug": DEBUG,
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
# https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
"loaders": [ "loaders": [
"django.template.loaders.filesystem.Loader", "django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader", "django.template.loaders.app_directories.Loader",
], ],
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors # See:
# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
"context_processors": [ "context_processors": [
"django.template.context_processors.debug", "django.template.context_processors.debug",
"django.template.context_processors.request", "django.template.context_processors.request",
...@@ -252,22 +543,135 @@ TEMPLATES = [ ...@@ -252,22 +543,135 @@ TEMPLATES = [
} }
] ]
# See: http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs # See:
# http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs
CRISPY_TEMPLATE_PACK = "bootstrap3" CRISPY_TEMPLATE_PACK = "bootstrap3"
# STATIC FILE CONFIGURATION # STATIC FILE CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR("staticfiles"))) STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR("staticfiles")))
"""
The path where static files are collected.
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = env("STATIC_URL", default="/staticfiles/") STATIC_URL = env("STATIC_URL", default=FUNKWHALE_URL + "/staticfiles/")
DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIFileSystemStorage" STORAGES = {
"default": {
"BACKEND": "funkwhale_api.common.storage.ASCIIFileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
PROXY_MEDIA = env.bool("PROXY_MEDIA", default=True)
"""
Whether to proxy audio files through your reverse proxy.
We recommend you leave this enabled to enforce access control.
If you're using S3 storage with :attr:`AWS_QUERYSTRING_AUTH`
enabled, it's safe to disable this setting.
"""
AWS_DEFAULT_ACL = env("AWS_DEFAULT_ACL", default=None)
"""
The default ACL to use when uploading files to an S3-compatible object storage
bucket.
ACLs and bucket policies are distinct concepts, and some storage
providers (ie Linode, Scaleway) will always apply the most restrictive between
a bucket's ACL and policy, meaning a default private ACL will supersede
a relaxed bucket policy.
If present, the value should be a valid canned ACL.
See `<https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl>`_
"""
AWS_QUERYSTRING_AUTH = env.bool("AWS_QUERYSTRING_AUTH", default=not PROXY_MEDIA)
"""
Whether to include signatures in S3 URLs. Signatures
are used to enforce access control.
Defaults to the opposite of :attr:`PROXY_MEDIA`.
"""
AWS_S3_MAX_MEMORY_SIZE = env.int(
"AWS_S3_MAX_MEMORY_SIZE", default=1000 * 1000 * 1000 * 20
)
AWS_QUERYSTRING_EXPIRE = env.int("AWS_QUERYSTRING_EXPIRE", default=3600)
"""
The time in seconds before AWS signatures expire.
Only takes effect you enable :attr:`AWS_QUERYSTRING_AUTH`
"""
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default=None)
"""
Access-key ID for your S3 storage.
"""
if AWS_ACCESS_KEY_ID:
AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
"""
Secret access key for your S3 storage.
"""
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
"""
Your S3 bucket name.
"""
AWS_S3_CUSTOM_DOMAIN = env("AWS_S3_CUSTOM_DOMAIN", default=None)
"""
Custom domain for serving your S3 files.
Useful if your provider offers a CDN-like service for your bucket.
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS .. important::
The URL must not contain a scheme (:attr:`AWS_S3_URL_PROTOCOL` is
automatically prepended) nor a trailing slash.
"""
AWS_S3_URL_PROTOCOL = env("AWS_S3_URL_PROTOCOL", default="https:")
"""
Protocol to use when constructing the custom domain (see :attr:`AWS_S3_CUSTOM_DOMAIN`)
.. important::
It must end with a `:`, remove `//`.
"""
AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", default=None)
"""
If you use a S3-compatible storage such as minio,
set the following variable to the full URL to the storage server.
Examples:
- ``https://minio.mydomain.com``
- ``https://s3.wasabisys.com``
"""
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default=None)
"""
If you're using Amazon S3 to serve media without a proxy,
you need to specify your region name to access files.
Example:
- ``eu-west-2``
"""
AWS_S3_SIGNATURE_VERSION = "s3v4"
AWS_LOCATION = env("AWS_LOCATION", default="")
"""
A directory in your S3 bucket where you store files.
Use this if you plan to share the bucket between services.
"""
STORAGES["default"]["BACKEND"] = "funkwhale_api.common.storage.ASCIIS3Boto3Storage"
# See:
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = (str(APPS_DIR.path("static")),) STATICFILES_DIRS = (str(APPS_DIR.path("static")),)
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders # See:
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder",
...@@ -277,31 +681,64 @@ STATICFILES_FINDERS = ( ...@@ -277,31 +681,64 @@ STATICFILES_FINDERS = (
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR("media"))) MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR("media")))
"""
The path where you store media files (such as album covers or audio tracks)
on your system. Make sure this directory actually exists.
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = env("MEDIA_URL", default="/media/") MEDIA_URL = env("MEDIA_URL", default=FUNKWHALE_URL + "/media/")
"""
The URL from which your pod serves media files. Change this if you're hosting media
files on a separate domain, or if you host Funkwhale on a non-standard port.
"""
FILE_UPLOAD_PERMISSIONS = 0o644
ATTACHMENTS_UNATTACHED_PRUNE_DELAY = env.int(
"ATTACHMENTS_UNATTACHED_PRUNE_DELAY", default=3600 * 24
)
"""
The delay in seconds before Funkwhale prunes uploaded but detached attachments
from the system.
"""
# URL Configuration # URL Configuration
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
ROOT_URLCONF = "config.urls" ROOT_URLCONF = "config.urls"
# See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application SPA_URLCONF = "config.urls.spa"
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.routing.application" ASGI_APPLICATION = "config.routing.application"
# This ensures that Django will be able to detect a secure connection # This ensures that Django will be able to detect a secure connection
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
# AUTHENTICATION CONFIGURATION # AUTHENTICATION CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend", "funkwhale_api.users.auth_backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend", "funkwhale_api.users.auth_backends.AllAuthBackend",
) )
SESSION_COOKIE_HTTPONLY = False SESSION_COOKIE_HTTPONLY = False
SESSION_COOKIE_AGE = env.int("SESSION_COOKIE_AGE", default=3600 * 25 * 60)
# Some really nice defaults # Some really nice defaults
ACCOUNT_AUTHENTICATION_METHOD = "username_email" ACCOUNT_AUTHENTICATION_METHOD = "username_email"
ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION_ENFORCE = env.bool(
"ACCOUNT_EMAIL_VERIFICATION_ENFORCE", default=False
)
"""
Set whether users need to verify their email address before using your pod. Enabling this setting
is useful for reducing spam and bot accounts. To use this setting you need to configure a mail server
to send verification emails. See :attr:`EMAIL_CONFIG`.
.. note::
Superusers created through the command line never need to verify their email address.
"""
ACCOUNT_EMAIL_VERIFICATION = (
"mandatory" if ACCOUNT_EMAIL_VERIFICATION_ENFORCE else "optional"
)
ACCOUNT_USERNAME_VALIDATORS = "funkwhale_api.users.serializers.username_validators"
# Custom user app defaults # Custom user app defaults
# Select the correct user model # Select the correct user model
...@@ -309,56 +746,302 @@ AUTH_USER_MODEL = "users.User" ...@@ -309,56 +746,302 @@ AUTH_USER_MODEL = "users.User"
LOGIN_REDIRECT_URL = "users:redirect" LOGIN_REDIRECT_URL = "users:redirect"
LOGIN_URL = "account_login" LOGIN_URL = "account_login"
# OAuth configuration
from funkwhale_api.users.oauth import scopes # noqa
OAUTH2_PROVIDER = {
"SCOPES": {s.id: s.label for s in scopes.SCOPES_BY_ID.values()},
"ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https", "urn"],
# we keep expired tokens for 15 days, for tracability
"REFRESH_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 15,
"AUTHORIZATION_CODE_EXPIRE_SECONDS": 5 * 60,
"ACCESS_TOKEN_EXPIRE_SECONDS": env.int(
"ACCESS_TOKEN_EXPIRE_SECONDS", default=60 * 60 * 10
),
"OAUTH2_SERVER_CLASS": "funkwhale_api.users.oauth.server.OAuth2Server",
"PKCE_REQUIRED": False,
}
OAUTH2_PROVIDER_APPLICATION_MODEL = "users.Application"
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "users.AccessToken"
OAUTH2_PROVIDER_GRANT_MODEL = "users.Grant"
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "users.RefreshToken"
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "users.IdToken"
SCOPED_TOKENS_MAX_AGE = 60 * 60 * 24 * 3
# LDAP AUTHENTICATION CONFIGURATION
# ------------------------------------------------------------------------------
AUTH_LDAP_ENABLED = env.bool("LDAP_ENABLED", default=False)
"""
Whether to enable LDAP authentication.
See :doc:`/administrator_documentation/configuration_docs/ldap` for more information.
"""
if AUTH_LDAP_ENABLED:
# Import the LDAP modules here.
# This way, we don't need the dependency unless someone
# actually enables the LDAP support
import ldap
from django_auth_ldap.config import GroupOfNamesType, LDAPSearch, LDAPSearchUnion
# Add LDAP to the authentication backends
AUTHENTICATION_BACKENDS += ("django_auth_ldap.backend.LDAPBackend",)
# Basic configuration
AUTH_LDAP_SERVER_URI = env("LDAP_SERVER_URI")
AUTH_LDAP_BIND_DN = env("LDAP_BIND_DN", default="")
AUTH_LDAP_BIND_PASSWORD = env("LDAP_BIND_PASSWORD", default="")
AUTH_LDAP_SEARCH_FILTER = env("LDAP_SEARCH_FILTER", default="(uid={0})").format(
"%(user)s"
)
AUTH_LDAP_START_TLS = env.bool("LDAP_START_TLS", default=False)
AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = env(
"AUTH_LDAP_BIND_AS_AUTHENTICATING_USER", default=False
)
DEFAULT_USER_ATTR_MAP = [
"first_name:givenName",
"last_name:sn",
"username:cn",
"email:mail",
]
LDAP_USER_ATTR_MAP = env.list("LDAP_USER_ATTR_MAP", default=DEFAULT_USER_ATTR_MAP)
AUTH_LDAP_USER_ATTR_MAP = {}
for m in LDAP_USER_ATTR_MAP:
funkwhale_field, ldap_field = m.split(":")
AUTH_LDAP_USER_ATTR_MAP[funkwhale_field.strip()] = ldap_field.strip()
# Determine root DN supporting multiple root DNs
AUTH_LDAP_ROOT_DN = env("LDAP_ROOT_DN")
AUTH_LDAP_ROOT_DN_LIST = []
for ROOT_DN in AUTH_LDAP_ROOT_DN.split():
AUTH_LDAP_ROOT_DN_LIST.append(
LDAPSearch(ROOT_DN, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER)
)
# Search for the user in all the root DNs
AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*AUTH_LDAP_ROOT_DN_LIST)
# Search for group types
LDAP_GROUP_DN = env("LDAP_GROUP_DN", default="")
if LDAP_GROUP_DN:
AUTH_LDAP_GROUP_DN = LDAP_GROUP_DN
# Get filter
AUTH_LDAP_GROUP_FILTER = env("LDAP_GROUP_FILER", default="")
# Search for the group in the specified DN
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
AUTH_LDAP_GROUP_DN, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_FILTER
)
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
# Configure basic group support
LDAP_REQUIRE_GROUP = env("LDAP_REQUIRE_GROUP", default="")
if LDAP_REQUIRE_GROUP:
AUTH_LDAP_REQUIRE_GROUP = LDAP_REQUIRE_GROUP
LDAP_DENY_GROUP = env("LDAP_DENY_GROUP", default="")
if LDAP_DENY_GROUP:
AUTH_LDAP_DENY_GROUP = LDAP_DENY_GROUP
# SLUGLIFIER # SLUGLIFIER
AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify" AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify"
CACHE_DEFAULT = "redis://127.0.0.1:6379/0" CACHE_URL_DEFAULT = "redis://127.0.0.1:6379/0"
CACHES = {"default": env.cache_url("CACHE_URL", default=CACHE_DEFAULT)} if IS_DOCKER_SETUP:
CACHE_URL_DEFAULT = "redis://redis:6379/0"
CACHES["default"]["BACKEND"] = "django_redis.cache.RedisCache" CACHE_URL = env.str("CACHE_URL", default=CACHE_URL_DEFAULT)
"""
The URL of your redis server. For example:
- ``redis://<host>:<port>/<database>``
- ``redis://127.0.0.1:6379/0``
- ``redis://:password@localhost:6379/0``
If you're using password auth (the extra slash is important)
- ``redis:///run/redis/redis.sock?db=0`` over unix sockets
.. note::
If you want to use Redis over unix sockets, you also need to update
:attr:`CELERY_BROKER_URL`, because the scheme differs from the one used by
:attr:`CACHE_URL`.
"""
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": CACHE_URL,
"OPTIONS": {
"CLIENT_CLASS": "funkwhale_api.common.cache.RedisClient",
"IGNORE_EXCEPTIONS": True, # mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
},
},
"local": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "local-cache",
},
}
cache_url = urlparse(CACHES["default"]["LOCATION"])
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "channels_redis.core.RedisChannelLayer", "BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {"hosts": [(cache_url.hostname, cache_url.port)]}, "CONFIG": {"hosts": [CACHES["default"]["LOCATION"]]},
} }
} }
CACHES["default"]["OPTIONS"] = { CACHEOPS_DURATION = env("CACHEOPS_DURATION", default=0)
"CLIENT_CLASS": "django_redis.client.DefaultClient", CACHEOPS_ENABLED = bool(CACHEOPS_DURATION)
"IGNORE_EXCEPTIONS": True, # mimics memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
}
if CACHEOPS_ENABLED:
INSTALLED_APPS += ("cacheops",)
CACHEOPS_REDIS = CACHE_URL
CACHEOPS_PREFIX = lambda _: "cacheops" # noqa
CACHEOPS_DEFAULTS = {"timeout": CACHEOPS_DURATION}
CACHEOPS = {
"music.album": {"ops": "count"},
"music.artist": {"ops": "count"},
"music.track": {"ops": "count"},
}
# CELERY # CELERY
INSTALLED_APPS += ("funkwhale_api.taskapp.celery.CeleryConfig",) INSTALLED_APPS += ("funkwhale_api.taskapp.celery.CeleryConfig",)
CELERY_BROKER_URL = env( CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default=CACHE_URL)
"CELERY_BROKER_URL", default=env("CACHE_URL", default=CACHE_DEFAULT) """
) The celery task broker URL. Defaults to :attr:`CACHE_URL`.
You don't need to tweak this unless you want
to use a different server or use Redis sockets to connect.
Example:
- ``unix://127.0.0.1:6379/0``
- ``redis+socket:///run/redis/redis.sock?virtual_host=0``
"""
# END CELERY # END CELERY
# Location of root django.contrib.admin URL, use {% url 'admin:index' %} # Location of root django.contrib.admin URL, use {% url 'admin:index' %}
# Your common stuff: Below this line define 3rd party library settings # Your common stuff: Below this line define 3rd party library settings
CELERY_TASK_DEFAULT_RATE_LIMIT = 1 CELERY_TASK_DEFAULT_RATE_LIMIT = 1
CELERY_TASK_TIME_LIMIT = 300 CELERY_TASK_TIME_LIMIT = env.int("CELERY_TASK_TIME_LIMIT", default=300)
CELERYBEAT_SCHEDULE = { CELERY_BEAT_SCHEDULE = {
"audio.fetch_rss_feeds": {
"task": "audio.fetch_rss_feeds",
"schedule": crontab(minute="0", hour="*"),
"options": {"expires": 60 * 60},
},
"common.prune_unattached_attachments": {
"task": "common.prune_unattached_attachments",
"schedule": crontab(minute="0", hour="*"),
"options": {"expires": 60 * 60},
},
"federation.clean_music_cache": { "federation.clean_music_cache": {
"task": "funkwhale_api.federation.tasks.clean_music_cache", "task": "federation.clean_music_cache",
"schedule": crontab(hour="*/2"), "schedule": crontab(minute="0", hour="*/2"),
"options": {"expires": 60 * 2},
},
"music.clean_transcoding_cache": {
"task": "music.clean_transcoding_cache",
"schedule": crontab(minute="0", hour="*"),
"options": {"expires": 60 * 2}, "options": {"expires": 60 * 2},
},
"oauth.clear_expired_tokens": {
"task": "oauth.clear_expired_tokens",
"schedule": crontab(minute="0", hour="0"),
"options": {"expires": 60 * 60 * 24},
},
"federation.refresh_nodeinfo_known_nodes": {
"task": "federation.refresh_nodeinfo_known_nodes",
"schedule": crontab(
**env.dict(
"SCHEDULE_FEDERATION_REFRESH_NODEINFO_KNOWN_NODES",
default={"minute": "0", "hour": "*"},
)
),
"options": {"expires": 60 * 60},
},
"music.library.schedule_remote_scan": {
"task": "music.library.schedule_scan",
"schedule": crontab(day_of_week="1", minute="0", hour="2"),
"options": {"expires": 60 * 60 * 24},
},
"federation.check_all_remote_instance_availability": {
"task": "federation.check_all_remote_instance_availability",
"schedule": crontab(
**env.dict(
"SCHEDULE_FEDERATION_CHECK_INTANCES_AVAILABILITY",
default={"minute": "0", "hour": "*"},
)
),
"options": {"expires": 60 * 60},
},
"listenbrainz.trigger_listening_sync_with_listenbrainz": {
"task": "listenbrainz.trigger_listening_sync_with_listenbrainz",
"schedule": crontab(day_of_week="*", minute="0", hour="3"),
"options": {"expires": 60 * 60 * 24},
},
"listenbrainz.trigger_favorite_sync_with_listenbrainz": {
"task": "listenbrainz.trigger_favorite_sync_with_listenbrainz",
"schedule": crontab(day_of_week="*", minute="0", hour="3"),
"options": {"expires": 60 * 60 * 24},
},
"tags.update_musicbrainz_genre": {
"task": "tags.update_musicbrainz_genre",
"schedule": crontab(day_of_month="2", minute="30", hour="3"),
"options": {"expires": 60 * 60 * 24},
},
} }
if env.str("TYPESENSE_API_KEY", default=None):
CELERY_BEAT_SCHEDULE["typesense.build_canonical_index"] = {
"task": "typesense.build_canonical_index",
"schedule": crontab(day_of_week="*/2", minute="0", hour="3"),
"options": {"expires": 60 * 60 * 24},
} }
JWT_AUTH = { if env.bool("ADD_ALBUM_TAGS_FROM_TRACKS", default=True):
"JWT_ALLOW_REFRESH": True, CELERY_BEAT_SCHEDULE["music.albums_set_tags_from_tracks"] = {
"JWT_EXPIRATION_DELTA": datetime.timedelta(days=7), "task": "music.albums_set_tags_from_tracks",
"JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=30), "schedule": crontab(minute="0", hour="4", day_of_week="4"),
"JWT_AUTH_HEADER_PREFIX": "JWT", "options": {"expires": 60 * 60 * 2},
"JWT_GET_USER_SECRET_KEY": lambda user: user.secret_key,
} }
if env.bool("ADD_ARTIST_TAGS_FROM_TRACKS", default=True):
CELERY_BEAT_SCHEDULE["music.artists_set_tags_from_tracks"] = {
"task": "music.artists_set_tags_from_tracks",
"schedule": crontab(minute="0", hour="4", day_of_week="4"),
"options": {"expires": 60 * 60 * 2},
}
NODEINFO_REFRESH_DELAY = env.int("NODEINFO_REFRESH_DELAY", default=3600 * 24)
def get_user_secret_key(user):
from django.conf import settings
return settings.SECRET_KEY + str(user.secret_key)
OLD_PASSWORD_FIELD_ENABLED = True OLD_PASSWORD_FIELD_ENABLED = True
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": env.int("PASSWORD_MIN_LENGTH", default=8)},
},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
DISABLE_PASSWORD_VALIDATORS = env.bool("DISABLE_PASSWORD_VALIDATORS", default=False)
"""
Whether to disable password validation rules during registration.
Validators include password length, common words, similarity with username.
"""
if DISABLE_PASSWORD_VALIDATORS:
AUTH_PASSWORD_VALIDATORS = []
ACCOUNT_ADAPTER = "funkwhale_api.users.adapters.FunkwhaleAccountAdapter" ACCOUNT_ADAPTER = "funkwhale_api.users.adapters.FunkwhaleAccountAdapter"
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True
# CORS_ORIGIN_WHITELIST = ( # CORS_ORIGIN_WHITELIST = (
...@@ -368,7 +1051,6 @@ CORS_ORIGIN_ALLOW_ALL = True ...@@ -368,7 +1051,6 @@ CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
REST_FRAMEWORK = { REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_PAGINATION_CLASS": "funkwhale_api.common.pagination.FunkwhalePagination", "DEFAULT_PAGINATION_CLASS": "funkwhale_api.common.pagination.FunkwhalePagination",
"PAGE_SIZE": 25, "PAGE_SIZE": 25,
"DEFAULT_PARSER_CLASSES": ( "DEFAULT_PARSER_CLASSES": (
...@@ -378,69 +1060,256 @@ REST_FRAMEWORK = { ...@@ -378,69 +1060,256 @@ REST_FRAMEWORK = {
"funkwhale_api.federation.parsers.ActivityParser", "funkwhale_api.federation.parsers.ActivityParser",
), ),
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": (
"funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS", "funkwhale_api.common.authentication.OAuth2Authentication",
"funkwhale_api.common.authentication.BearerTokenHeaderAuth", "funkwhale_api.common.authentication.ApplicationTokenAuthentication",
"rest_framework_jwt.authentication.JSONWebTokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication", "rest_framework.authentication.BasicAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": (
"funkwhale_api.users.oauth.permissions.ScopePermission",
), ),
"DEFAULT_FILTER_BACKENDS": ( "DEFAULT_FILTER_BACKENDS": (
"rest_framework.filters.OrderingFilter", "rest_framework.filters.OrderingFilter",
"django_filters.rest_framework.DjangoFilterBackend", "django_filters.rest_framework.DjangoFilterBackend",
), ),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
"NUM_PROXIES": env.int("NUM_PROXIES", default=1),
}
THROTTLING_ENABLED = env.bool("THROTTLING_ENABLED", default=True)
"""
Whether to enable throttling (also known as rate-limiting).
We recommend you leave this enabled to improve the quality
of the service, especially on public pods .
"""
if THROTTLING_ENABLED:
REST_FRAMEWORK["DEFAULT_THROTTLE_CLASSES"] = env.list(
"THROTTLE_CLASSES",
default=["funkwhale_api.common.throttling.FunkwhaleThrottle"],
)
THROTTLING_SCOPES = {
"*": {"anonymous": "anonymous-wildcard", "authenticated": "authenticated-wildcard"},
"create": {
"authenticated": "authenticated-create",
"anonymous": "anonymous-create",
},
"list": {"authenticated": "authenticated-list", "anonymous": "anonymous-list"},
"retrieve": {
"authenticated": "authenticated-retrieve",
"anonymous": "anonymous-retrieve",
},
"destroy": {
"authenticated": "authenticated-destroy",
"anonymous": "anonymous-destroy",
},
"update": {
"authenticated": "authenticated-update",
"anonymous": "anonymous-update",
},
"partial_update": {
"authenticated": "authenticated-update",
"anonymous": "anonymous-update",
},
} }
THROTTLING_USER_RATES = env.dict("THROTTLING_RATES", default={})
THROTTLING_RATES = {
"anonymous-wildcard": {
"rate": THROTTLING_USER_RATES.get("anonymous-wildcard", "1000/h"),
"description": "Anonymous requests not covered by other limits",
},
"authenticated-wildcard": {
"rate": THROTTLING_USER_RATES.get("authenticated-wildcard", "2000/h"),
"description": "Authenticated requests not covered by other limits",
},
"authenticated-create": {
"rate": THROTTLING_USER_RATES.get("authenticated-create", "1000/hour"),
"description": "Authenticated POST requests",
},
"anonymous-create": {
"rate": THROTTLING_USER_RATES.get("anonymous-create", "1000/day"),
"description": "Anonymous POST requests",
},
"authenticated-list": {
"rate": THROTTLING_USER_RATES.get("authenticated-list", "10000/hour"),
"description": "Authenticated GET requests on resource lists",
},
"anonymous-list": {
"rate": THROTTLING_USER_RATES.get("anonymous-list", "10000/day"),
"description": "Anonymous GET requests on resource lists",
},
"authenticated-retrieve": {
"rate": THROTTLING_USER_RATES.get("authenticated-retrieve", "10000/hour"),
"description": "Authenticated GET requests on resource detail",
},
"anonymous-retrieve": {
"rate": THROTTLING_USER_RATES.get("anonymous-retrieve", "10000/day"),
"description": "Anonymous GET requests on resource detail",
},
"authenticated-destroy": {
"rate": THROTTLING_USER_RATES.get("authenticated-destroy", "500/hour"),
"description": "Authenticated DELETE requests on resource detail",
},
"anonymous-destroy": {
"rate": THROTTLING_USER_RATES.get("anonymous-destroy", "1000/day"),
"description": "Anonymous DELETE requests on resource detail",
},
"authenticated-update": {
"rate": THROTTLING_USER_RATES.get("authenticated-update", "1000/hour"),
"description": "Authenticated PATCH and PUT requests on resource detail",
},
"anonymous-update": {
"rate": THROTTLING_USER_RATES.get("anonymous-update", "1000/day"),
"description": "Anonymous PATCH and PUT requests on resource detail",
},
"subsonic": {
"rate": THROTTLING_USER_RATES.get("subsonic", "2000/hour"),
"description": "All subsonic API requests",
},
# potentially spammy / dangerous endpoints
"authenticated-reports": {
"rate": THROTTLING_USER_RATES.get("authenticated-reports", "100/day"),
"description": "Authenticated report submission",
},
"anonymous-reports": {
"rate": THROTTLING_USER_RATES.get("anonymous-reports", "10/day"),
"description": "Anonymous report submission",
},
"authenticated-oauth-app": {
"rate": THROTTLING_USER_RATES.get("authenticated-oauth-app", "10/hour"),
"description": "Authenticated OAuth app creation",
},
"anonymous-oauth-app": {
"rate": THROTTLING_USER_RATES.get("anonymous-oauth-app", "10/day"),
"description": "Anonymous OAuth app creation",
},
"oauth-authorize": {
"rate": THROTTLING_USER_RATES.get("oauth-authorize", "100/hour"),
"description": "OAuth app authorization",
},
"oauth-token": {
"rate": THROTTLING_USER_RATES.get("oauth-token", "100/hour"),
"description": "OAuth token creation",
},
"oauth-revoke-token": {
"rate": THROTTLING_USER_RATES.get("oauth-revoke-token", "100/hour"),
"description": "OAuth token deletion",
},
"login": {
"rate": THROTTLING_USER_RATES.get("login", "30/hour"),
"description": "Login",
},
"signup": {
"rate": THROTTLING_USER_RATES.get("signup", "10/day"),
"description": "Account creation",
},
"verify-email": {
"rate": THROTTLING_USER_RATES.get("verify-email", "20/h"),
"description": "Email address confirmation",
},
"password-change": {
"rate": THROTTLING_USER_RATES.get("password-change", "20/h"),
"description": "Password change (when authenticated)",
},
"password-reset": {
"rate": THROTTLING_USER_RATES.get("password-reset", "20/h"),
"description": "Password reset request",
},
"password-reset-confirm": {
"rate": THROTTLING_USER_RATES.get("password-reset-confirm", "20/h"),
"description": "Password reset confirmation",
},
"fetch": {
"rate": THROTTLING_USER_RATES.get("fetch", "200/d"),
"description": "Fetch remote objects",
},
}
THROTTLING_RATES = THROTTLING_RATES
"""
Throttling rates for specific endpoints and app features.
Tweak this if you're hitting rate limit issues or if you want
to reduce the consumption of specific endpoints. Takes
the format ``<endpoint name>=<number>/<interval>``.
Example:
- ``signup=5/d,password-reset=2/d,anonymous-reports=5/d``
"""
BROWSABLE_API_ENABLED = env.bool("BROWSABLE_API_ENABLED", default=False) BROWSABLE_API_ENABLED = env.bool("BROWSABLE_API_ENABLED", default=False)
if BROWSABLE_API_ENABLED: if BROWSABLE_API_ENABLED:
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] += ( REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] += (
"rest_framework.renderers.BrowsableAPIRenderer", "rest_framework.renderers.BrowsableAPIRenderer",
) )
REST_AUTH_SERIALIZERS = { REST_AUTH = {
"PASSWORD_RESET_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetSerializer" # noqa "PASSWORD_RESET_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetSerializer", # noqa
"PASSWORD_RESET_CONFIRM_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetConfirmSerializer", # noqa
} }
REST_SESSION_LOGIN = False REST_SESSION_LOGIN = False
REST_USE_JWT = True
ATOMIC_REQUESTS = False ATOMIC_REQUESTS = False
USE_X_FORWARDED_HOST = True USE_X_FORWARDED_HOST = True
USE_X_FORWARDED_PORT = True USE_X_FORWARDED_PORT = True
# Wether we should use Apache, Nginx (or other) headers when serving audio files # Whether we should use Apache, Nginx (or other) headers
# Default to Nginx # when serving audio files. Defaults to Nginx.
REVERSE_PROXY_TYPE = env("REVERSE_PROXY_TYPE", default="nginx") REVERSE_PROXY_TYPE = env("REVERSE_PROXY_TYPE", default="nginx")
"""
Set your reverse proxy type. This changes the headers the
API uses to serve audio files. Allowed values:
- ``nginx``
- ``apache2``
"""
assert REVERSE_PROXY_TYPE in ["apache2", "nginx"], "Unsupported REVERSE_PROXY_TYPE" assert REVERSE_PROXY_TYPE in ["apache2", "nginx"], "Unsupported REVERSE_PROXY_TYPE"
# Which path will be used to process the internal redirection
# **DO NOT** put a slash at the end
PROTECT_FILES_PATH = env("PROTECT_FILES_PATH", default="/_protected") PROTECT_FILES_PATH = env("PROTECT_FILES_PATH", default="/_protected")
"""
The path used to process internal redirection
to the reverse proxy.
.. important::
Don't insert a slash at the end of this path.
"""
# use this setting to tweak for how long you want to cache
# musicbrainz results. (value is in seconds)
MUSICBRAINZ_CACHE_DURATION = env.int("MUSICBRAINZ_CACHE_DURATION", default=300) MUSICBRAINZ_CACHE_DURATION = env.int("MUSICBRAINZ_CACHE_DURATION", default=300)
CACHEOPS_REDIS = env("CACHE_URL", default=CACHE_DEFAULT) """
CACHEOPS_ENABLED = env.bool("CACHEOPS_ENABLED", default=True) Length of time in seconds to cache MusicBrainz results.
CACHEOPS = { """
"music.artist": {"ops": "all", "timeout": 60 * 60}, MUSICBRAINZ_HOSTNAME = env("MUSICBRAINZ_HOSTNAME", default="musicbrainz.org")
"music.album": {"ops": "all", "timeout": 60 * 60}, """
"music.track": {"ops": "all", "timeout": 60 * 60}, The hostname of your MusicBrainz instance. Change
"music.trackfile": {"ops": "all", "timeout": 60 * 60}, this setting if you run your own server or use a mirror.
"taggit.tag": {"ops": "all", "timeout": 60 * 60}, You can include a port number in the hostname.
}
Examples:
- ``mymusicbrainz.mirror``
- ``localhost:5000``
"""
# Custom Admin URL, use {% url 'admin:index' %} # Custom Admin URL, use {% url 'admin:index' %}
ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/") ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/")
CSRF_USE_SESSIONS = True """
Path to the Django admin dashboard.
# Playlist settings Examples:
# XXX: deprecated, see #186
PLAYLISTS_MAX_TRACKS = env.int("PLAYLISTS_MAX_TRACKS", default=250) - ``^api/admin/``
- ``^api/mycustompath/``
"""
CSRF_USE_SESSIONS = False
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
ACCOUNT_USERNAME_BLACKLIST = [ ACCOUNT_USERNAME_BLACKLIST = [
"funkwhale", "funkwhale",
"library", "library",
"instance",
"test", "test",
"status", "status",
"root", "root",
...@@ -449,19 +1318,235 @@ ACCOUNT_USERNAME_BLACKLIST = [ ...@@ -449,19 +1318,235 @@ ACCOUNT_USERNAME_BLACKLIST = [
"superuser", "superuser",
"staff", "staff",
"service", "service",
"me",
"ghost",
"_",
"-",
"hello",
"contact",
"inbox",
"outbox",
"shared-inbox",
"shared_inbox",
"actor",
] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[]) ] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[])
"""
List of usernames that can't be used for registration. Given as a list of strings.
"""
EXTERNAL_REQUESTS_VERIFY_SSL = env.bool("EXTERNAL_REQUESTS_VERIFY_SSL", default=True) EXTERNAL_REQUESTS_VERIFY_SSL = env.bool("EXTERNAL_REQUESTS_VERIFY_SSL", default=True)
# XXX: deprecated, see #186 """
API_AUTHENTICATION_REQUIRED = env.bool("API_AUTHENTICATION_REQUIRED", True) Whether to enforce TLS certificate verification
when performing outgoing HTTP requests.
We recommend you leave this setting enabled.
"""
EXTERNAL_REQUESTS_TIMEOUT = env.int("EXTERNAL_REQUESTS_TIMEOUT", default=10)
"""
Default timeout for external requests.
"""
MUSIC_DIRECTORY_PATH = env("MUSIC_DIRECTORY_PATH", default=None) MUSIC_DIRECTORY_PATH = env("MUSIC_DIRECTORY_PATH", default=None)
# on Docker setup, the music directory may not match the host path, """
# and we need to know it for it to serve stuff properly The path on your server where Funkwhale places
files from in-place imports. This path needs to be
readable by the webserver and ``api`` and ``worker``
processes.
.. important::
Don’t insert a slash at the end of this path.
On Docker installations, we recommend you use the default ``/music`` path.
On Debian installations you can use any absolute path. Defaults to
``/srv/funkwhale/data/music``.
.. note::
You need to add this path to your reverse proxy configuration.
Add the directory to your ``/_protected/music`` server block.
"""
MUSIC_DIRECTORY_SERVE_PATH = env( MUSIC_DIRECTORY_SERVE_PATH = env(
"MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH "MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH
) )
"""
On Docker setups the value of :attr:`MUSIC_DIRECTORY_PATH`
may be different from the actual path on your server.
You can specify this path in your :file:`docker-compose.yml` file::
volumes:
- /srv/funkwhale/data/music:/music:ro
In this case, you need to set :attr:`MUSIC_DIRECTORY_SERVE_PATH`
to ``/srv/funkwhale/data/music``. The webserver needs to be
able to read this directory.
.. important::
Don’t insert a slash at the end of this path.
"""
# When this is set to default=True, we need to re-enable migration music/0042
# to ensure data is populated correctly on existing pods
MUSIC_USE_DENORMALIZATION = env.bool("MUSIC_USE_DENORMALIZATION", default=True)
USERS_INVITATION_EXPIRATION_DAYS = env.int( USERS_INVITATION_EXPIRATION_DAYS = env.int(
"USERS_INVITATION_EXPIRATION_DAYS", default=14 "USERS_INVITATION_EXPIRATION_DAYS", default=14
) )
"""
The number of days before a user invite expires.
"""
VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
"square": [
("original", "url"),
("square_crop", "crop__400x400"),
("medium_square_crop", "crop__200x200"),
("small_square_crop", "crop__50x50"),
],
"attachment_square": [
("original", "url"),
("small_square_crop", "crop__50x50"),
("medium_square_crop", "crop__200x200"),
("large_square_crop", "crop__600x600"),
],
}
VERSATILEIMAGEFIELD_SETTINGS = {
"create_images_on_demand": False,
"jpeg_resize_quality": env.int("THUMBNAIL_JPEG_RESIZE_QUALITY", default=95),
}
RSA_KEY_SIZE = 2048
# for performance gain in tests, since we don't need to actually create the
# thumbnails
CREATE_IMAGE_THUMBNAILS = env.bool("CREATE_IMAGE_THUMBNAILS", default=True)
# we rotate actor keys at most every two days by default
ACTOR_KEY_ROTATION_DELAY = env.int("ACTOR_KEY_ROTATION_DELAY", default=3600 * 48)
SUBSONIC_DEFAULT_TRANSCODING_FORMAT = (
env("SUBSONIC_DEFAULT_TRANSCODING_FORMAT", default="mp3") or None
)
"""
The default format files are transcoded into when using the Subsonic
API.
"""
# extra tags will be ignored
TAGS_MAX_BY_OBJ = env.int("TAGS_MAX_BY_OBJ", default=30)
"""
Maximum number of tags that can be associated with an object.
Extra tags are ignored.
"""
FEDERATION_OBJECT_FETCH_DELAY = env.int(
"FEDERATION_OBJECT_FETCH_DELAY", default=60 * 24 * 3
)
"""
The delay in minutes before a remote object is automatically
refetched when accessed in the UI.
"""
MODERATION_EMAIL_NOTIFICATIONS_ENABLED = env.bool(
"MODERATION_EMAIL_NOTIFICATIONS_ENABLED", default=True
)
"""
Whether to enable email notifications to moderators and pod admins.
"""
FEDERATION_AUTHENTIFY_FETCHES = True
FEDERATION_SYNCHRONOUS_FETCH = env.bool("FEDERATION_SYNCHRONOUS_FETCH", default=True)
FEDERATION_DUPLICATE_FETCH_DELAY = env.int(
"FEDERATION_DUPLICATE_FETCH_DELAY", default=60 * 50
)
"""
The delay in seconds between two manual fetches of the same remote object.
"""
INSTANCE_SUPPORT_MESSAGE_DELAY = env.int("INSTANCE_SUPPORT_MESSAGE_DELAY", default=15)
"""
The number of days before your pod shows the "support your pod" message.
The timer starts after the user signs up.
"""
FUNKWHALE_SUPPORT_MESSAGE_DELAY = env.int("FUNKWHALE_SUPPORT_MESSAGE_DELAY", default=15)
"""
The number of days before your pod shows the "support Funkwhale" message.
The timer starts after the user signs up.
"""
MIN_DELAY_BETWEEN_DOWNLOADS_COUNT = env.int(
"MIN_DELAY_BETWEEN_DOWNLOADS_COUNT", default=60 * 60 * 6
)
"""
The required number of seconds between downloads of a track
by the same IP or user to be counted separately in listen statistics.
"""
MARKDOWN_EXTENSIONS = env.list("MARKDOWN_EXTENSIONS", default=["nl2br", "extra"])
"""
A list of markdown extensions to enable.
See `<https://python-markdown.github.io/extensions/>`_.
"""
LINKIFIER_SUPPORTED_TLDS = ["audio"] + env.list("LINKINFIER_SUPPORTED_TLDS", default=[])
"""
Additional TLDs to support with our markdown linkifier.
"""
EXTERNAL_MEDIA_PROXY_ENABLED = env.bool("EXTERNAL_MEDIA_PROXY_ENABLED", default=True)
"""
Whether to proxy attachment files hosted on third party pods and and servers.
We recommend you leave this set to ``true``. This reduces the risk of leaking
user browsing information and reduces the bandwidth used on remote pods.
"""
PODCASTS_THIRD_PARTY_VISIBILITY = env("PODCASTS_THIRD_PARTY_VISIBILITY", default="me")
"""
By default, only people who subscribe to a podcast RSS have access
to its episodes. Change to ``instance`` or ``everyone`` to change the
default visibility.
.. note::
Changing this value only affect new podcasts.
"""
PODCASTS_RSS_FEED_REFRESH_DELAY = env.int(
"PODCASTS_RSS_FEED_REFRESH_DELAY", default=60 * 60 * 24
)
"""
The delay in seconds between two fetch of RSS feeds.
A lower rate means new episodes are fetched sooner,
but requires more resources.
"""
# maximum items loaded through XML feed
PODCASTS_RSS_FEED_MAX_ITEMS = env.int("PODCASTS_RSS_FEED_MAX_ITEMS", default=250)
"""
Maximum number of RSS items to load in each podcast feed.
"""
IGNORE_FORWARDED_HOST_AND_PROTO = env.bool(
"IGNORE_FORWARDED_HOST_AND_PROTO", default=True
)
"""
Use :attr:`FUNKWHALE_HOSTNAME` and :attr:`FUNKWHALE_PROTOCOL`
instead of request header.
"""
HASHING_ALGORITHM = "sha256"
HASHING_CHUNK_SIZE = 1024 * 100
"""
Typenses Settings
"""
TYPESENSE_API_KEY = env("TYPESENSE_API_KEY", default=None)
""" Typesense API key. This need to be defined in the .env file for Typenses to be activated."""
TYPESENSE_PORT = env("TYPESENSE_PORT", default="8108")
"""Typesense listening port"""
TYPESENSE_PROTOCOL = env("TYPESENSE_PROTOCOL", default="http")
"""Typesense listening protocol"""
TYPESENSE_HOST = env(
"TYPESENSE_HOST",
default="typesense" if IS_DOCKER_SETUP else "localhost",
)
"""
Typesense hostname. Defaults to `localhost` on non-Docker deployments and to `typesense` on
Docker deployments.
"""
TYPESENSE_NUM_TYPO = env("TYPESENSE_NUM_TYPO", default=5)
"""
Max tracks to be downloaded when the THIRD_PARTY_UPLOAD plugin hook is triggered.
Each api request to playlist tracks or radio tracks trigger the hook if tracks upload are missing.
If your instance is big your ip might get rate limited.
"""
THIRD_PARTY_UPLOAD_MAX_UPLOADS = env.int("THIRD_PARTY_UPLOAD_MAX_UPLOADS", default=10)
# -*- coding: utf-8 -*-
""" """
Local settings Local settings
- Run in Debug mode - Run in Debug mode
- Use console backend for emails - Add Django Debug Toolbar when INTERNAL_IPS are given and match the request
- Add Django Debug Toolbar
- Add django-extensions as app - Add django-extensions as app
""" """
from .common import * # noqa from funkwhale_api import __version__ as funkwhale_version
from .common import * # noqa
# DEBUG # DEBUG
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DEBUG = env.bool("DJANGO_DEBUG", default=True) DEBUG = env.bool("DJANGO_DEBUG", default=True)
FORCE_HTTPS_URLS = env.bool("FORCE_HTTPS_URLS", default=False)
TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG
# SECRET CONFIGURATION # SECRET CONFIGURATION
...@@ -24,26 +24,60 @@ SECRET_KEY = env( ...@@ -24,26 +24,60 @@ SECRET_KEY = env(
"DJANGO_SECRET_KEY", default="mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc" "DJANGO_SECRET_KEY", default="mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc"
) )
# Mail settings
# ------------------------------------------------------------------------------
EMAIL_HOST = "localhost"
EMAIL_PORT = 1025
# django-debug-toolbar # django-debug-toolbar
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",)
# INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',) # INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',)
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {
"DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
"SHOW_TEMPLATE_CONTEXT": True, "SHOW_TEMPLATE_CONTEXT": True,
"SHOW_TOOLBAR_CALLBACK": lambda request: True, "SHOW_TOOLBAR_CALLBACK": lambda request: "debug" in request.GET,
"JQUERY_URL": "/staticfiles/admin/js/vendor/jquery/jquery.js",
} }
# DEBUG_TOOLBAR_PANELS = [
# 'debug_toolbar.panels.versions.VersionsPanel',
# 'debug_toolbar.panels.timer.TimerPanel',
# 'debug_toolbar.panels.settings.SettingsPanel',
# 'debug_toolbar.panels.headers.HeadersPanel',
# 'debug_toolbar.panels.request.RequestPanel',
# 'debug_toolbar.panels.sql.SQLPanel',
# 'debug_toolbar.panels.staticfiles.StaticFilesPanel',
# 'debug_toolbar.panels.templates.TemplatesPanel',
# 'debug_toolbar.panels.cache.CachePanel',
# 'debug_toolbar.panels.signals.SignalsPanel',
# 'debug_toolbar.panels.logging.LoggingPanel',
# 'debug_toolbar.panels.redirects.RedirectsPanel',
# 'debug_toolbar.panels.profiling.ProfilingPanel',
# 'debug_toolbar_line_profiler.panel.ProfilingPanel'
# ]
DEBUG_TOOLBAR_PANELS = [
# 'debug_toolbar.panels.versions.VersionsPanel',
"debug_toolbar.panels.timer.TimerPanel",
"debug_toolbar.panels.settings.SettingsPanel",
"debug_toolbar.panels.headers.HeadersPanel",
# 'debug_toolbar.panels.request.RequestPanel',
"debug_toolbar.panels.sql.SQLPanel",
# 'debug_toolbar.panels.staticfiles.StaticFilesPanel',
# 'debug_toolbar.panels.templates.TemplatesPanel',
"debug_toolbar.panels.cache.CachePanel",
# 'debug_toolbar.panels.signals.SignalsPanel',
# 'debug_toolbar.panels.logging.LoggingPanel',
# 'debug_toolbar.panels.redirects.RedirectsPanel',
# 'debug_toolbar.panels.profiling.ProfilingPanel',
]
# django-extensions # django-extensions
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# INSTALLED_APPS += ('django_extensions', ) INSTALLED_APPS += ("django_extensions",)
INSTALLED_APPS += ("drf_spectacular",)
# Debug toolbar is slow, we disable it for tests
DEBUG_TOOLBAR_ENABLED = env.bool("DEBUG_TOOLBAR_ENABLED", default=DEBUG)
if DEBUG_TOOLBAR_ENABLED:
MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",)
INSTALLED_APPS += ("debug_toolbar",) INSTALLED_APPS += ("debug_toolbar",)
# TESTING # TESTING
...@@ -51,22 +85,73 @@ INSTALLED_APPS += ("debug_toolbar",) ...@@ -51,22 +85,73 @@ INSTALLED_APPS += ("debug_toolbar",)
TEST_RUNNER = "django.test.runner.DiscoverRunner" TEST_RUNNER = "django.test.runner.DiscoverRunner"
# CELERY # CELERY
# In development, all tasks will be executed locally by blocking until the task returns
CELERY_TASK_ALWAYS_EAGER = False CELERY_TASK_ALWAYS_EAGER = False
# END CELERY # END CELERY
# Your local stuff: Below this line define 3rd party library settings # Your local stuff: Below this line define 3rd party library settings
LOGGING = { REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "funkwhale_api.schema.CustomAutoSchema"
"version": 1, SPECTACULAR_SETTINGS = {
"handlers": {"console": {"level": "DEBUG", "class": "logging.StreamHandler"}}, "TITLE": "Funkwhale API",
"loggers": { "DESCRIPTION": open("Readme.md").read(),
"django.request": { "VERSION": funkwhale_version,
"handlers": ["console"], "SCHEMA_PATH_PREFIX": "/api/(v[0-9])?",
"propagate": True, "OAUTH_FLOWS": ["authorizationCode"],
"level": "DEBUG", "AUTHENTICATION_WHITELIST": [
"funkwhale_api.common.authentication.OAuth2Authentication",
"funkwhale_api.common.authentication.ApplicationTokenAuthentication",
],
"SERVERS": [
{"url": "https://demo.funkwhale.audio", "description": "Demo Server"},
{
"url": "https://funkwhale.audio",
"description": "Read server with real content",
}, },
"": {"level": "DEBUG", "handlers": ["console"]}, {
"url": "{protocol}://{domain}",
"description": "Custom server",
"variables": {
"domain": {
"default": "yourdomain",
"description": "Your Funkwhale Domain",
}, },
"protocol": {"enum": ["http", "https"], "default": "https"},
},
},
],
"OAUTH2_FLOWS": ["authorizationCode"],
"OAUTH2_AUTHORIZATION_URL": "/authorize",
"OAUTH2_TOKEN_URL": "/api/v1/oauth/token/",
"PREPROCESSING_HOOKS": ["config.schema.custom_preprocessing_hook"],
"ENUM_NAME_OVERRIDES": {
"FederationChoiceEnum": "funkwhale_api.federation.models.TYPE_CHOICES",
"ReportTypeEnum": "funkwhale_api.moderation.models.REPORT_TYPES",
"PrivacyLevelEnum": "funkwhale_api.common.fields.PRIVACY_LEVEL_CHOICES",
"LibraryPrivacyLevelEnum": "funkwhale_api.music.models.LIBRARY_PRIVACY_LEVEL_CHOICES",
},
"COMPONENT_SPLIT_REQUEST": True,
} }
CSRF_TRUSTED_ORIGINS = [o for o in ALLOWED_HOSTS]
if env.bool("WEAK_PASSWORDS", default=False):
# Faster during tests
PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",)
MIDDLEWARE = (
"funkwhale_api.common.middleware.DevHttpsMiddleware",
"funkwhale_api.common.middleware.ProfilerMiddleware",
"funkwhale_api.common.middleware.PymallocMiddleware",
) + MIDDLEWARE
REST_FRAMEWORK.update(
{
"TEST_REQUEST_RENDERER_CLASSES": [
"rest_framework.renderers.MultiPartRenderer",
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.TemplateHTMLRenderer",
"funkwhale_api.playlists.renderers.PlaylistXspfRenderer",
],
}
)
# allows makemigrations and superuser creation
FORCE = env("FORCE", default=True)
# -*- coding: utf-8 -*-
""" """
Production Configurations Production Configurations
- Use djangosecure - Use djangosecure
- Use Amazon's S3 for storing static files and uploaded media - Use Amazon's S3 for storing static files and uploaded media
- Use mailgun to send emails - Use mailgun to send e-mails
- Use Redis on Heroku - Use Redis on Heroku
""" """
from __future__ import absolute_import, unicode_literals
from .common import * # noqa from .common import * # noqa
...@@ -43,24 +41,6 @@ SECRET_KEY = env("DJANGO_SECRET_KEY") ...@@ -43,24 +41,6 @@ SECRET_KEY = env("DJANGO_SECRET_KEY")
# SESSION_COOKIE_HTTPONLY = True # SESSION_COOKIE_HTTPONLY = True
# SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) # SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
# SITE CONFIGURATION
# ------------------------------------------------------------------------------
# Hosts/domain names that are valid for this site
# See https://docs.djangoproject.com/en/1.6/ref/settings/#allowed-hosts
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# END SITE CONFIGURATION
# STORAGE CONFIGURATION
# ------------------------------------------------------------------------------
# Uploaded Media Files
# ------------------------
DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
# Static Assets
# ------------------------
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
# TEMPLATE CONFIGURATION # TEMPLATE CONFIGURATION
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# See: # See:
...@@ -79,50 +59,4 @@ TEMPLATES[0]["OPTIONS"]["loaders"] = [ ...@@ -79,50 +59,4 @@ TEMPLATES[0]["OPTIONS"]["loaders"] = [
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Heroku URL does not pass the DB number, so we parse it in # Heroku URL does not pass the DB number, so we parse it in
# LOGGING CONFIGURATION
# ------------------------------------------------------------------------------
# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
"formatters": {
"verbose": {
"format": "%(levelname)s %(asctime)s %(module)s "
"%(process)d %(thread)d %(message)s"
}
},
"handlers": {
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
},
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"loggers": {
"django.request": {
"handlers": ["mail_admins"],
"level": "ERROR",
"propagate": True,
},
"django.security.DisallowedHost": {
"level": "ERROR",
"handlers": ["console", "mail_admins"],
"propagate": True,
},
},
}
# Your production stuff: Below this line define 3rd party library settings # Your production stuff: Below this line define 3rd party library settings
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.urls import include, path, re_path
from django.views import defaults as default_views from django.views import defaults as default_views
from config import plugins
from funkwhale_api.common import admin
plugins_patterns = plugins.trigger_filter(plugins.URLS, [], enabled=True)
api_patterns = [
re_path("v1/", include("config.urls.api")),
re_path("v2/", include("config.urls.api_v2")),
re_path("subsonic/", include("config.urls.subsonic")),
]
urlpatterns = [ urlpatterns = [
# Django Admin, use {% url 'admin:index' %} # Django Admin, use {% url 'admin:index' %}
url(settings.ADMIN_URL, admin.site.urls), re_path(settings.ADMIN_URL, admin.site.urls),
url(r"^api/", include(("config.api_urls", "api"), namespace="api")), re_path(r"^api/", include((api_patterns, "api"), namespace="api")),
url( re_path(
r"^", r"^",
include( include(
("funkwhale_api.federation.urls", "federation"), namespace="federation" ("funkwhale_api.federation.urls", "federation"), namespace="federation"
), ),
), ),
url(r"^api/v1/auth/", include("rest_auth.urls")), re_path(
url(r"^api/v1/auth/registration/", include("funkwhale_api.users.rest_auth_urls")), r"^api/v1/auth/",
url(r"^accounts/", include("allauth.urls")), include("funkwhale_api.users.rest_auth_urls"),
# Your stuff: custom urls includes go here ),
] re_path(
r"^api/v2/auth/",
include("funkwhale_api.users.rest_auth_urls"),
),
re_path(r"^accounts/", include("allauth.urls")),
] + plugins_patterns
if settings.DEBUG: if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit # This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like. # these url in browser to see how these error pages look like.
urlpatterns += [ urlpatterns += [
url(r"^400/$", default_views.bad_request), re_path(r"^400/$", default_views.bad_request),
url(r"^403/$", default_views.permission_denied), re_path(r"^403/$", default_views.permission_denied),
url(r"^404/$", default_views.page_not_found), re_path(r"^404/$", default_views.page_not_found),
url(r"^500/$", default_views.server_error), re_path(r"^500/$", default_views.server_error),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if "debug_toolbar" in settings.INSTALLED_APPS: if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar import debug_toolbar
urlpatterns += [url(r"^__debug__/", include(debug_toolbar.urls))] urlpatterns = [
path("api/__debug__/", include(debug_toolbar.urls))
] + urlpatterns
if "silk" in settings.INSTALLED_APPS:
urlpatterns = [
re_path(r"^api/silk/", include("silk.urls", namespace="silk"))
] + urlpatterns
from django.conf.urls import include, url from django.conf.urls import include
from dynamic_preferences.api.viewsets import GlobalPreferencesViewSet from django.urls import re_path
from rest_framework import routers
from rest_framework.urlpatterns import format_suffix_patterns
from rest_framework_jwt import views as jwt_views
from funkwhale_api.activity import views as activity_views from funkwhale_api.activity import views as activity_views
from funkwhale_api.audio import views as audio_views
from funkwhale_api.common import routers as common_routers
from funkwhale_api.common import views as common_views
from funkwhale_api.music import views from funkwhale_api.music import views
from funkwhale_api.playlists import views as playlists_views from funkwhale_api.playlists import views as playlists_views
from funkwhale_api.subsonic.views import SubsonicViewSet from funkwhale_api.tags import views as tags_views
router = routers.SimpleRouter() router = common_routers.OptionalSlashRouter()
router.register(r"settings", GlobalPreferencesViewSet, base_name="settings")
router.register(r"activity", activity_views.ActivityViewSet, "activity") router.register(r"activity", activity_views.ActivityViewSet, "activity")
router.register(r"tags", views.TagViewSet, "tags") router.register(r"tags", tags_views.TagViewSet, "tags")
router.register(r"plugins", common_views.PluginViewSet, "plugins")
router.register(r"tracks", views.TrackViewSet, "tracks") router.register(r"tracks", views.TrackViewSet, "tracks")
router.register(r"trackfiles", views.TrackFileViewSet, "trackfiles") router.register(r"uploads", views.UploadViewSet, "uploads")
router.register(r"libraries", views.LibraryViewSet, "libraries")
router.register(r"listen", views.ListenViewSet, "listen")
router.register(r"stream", views.StreamViewSet, "stream")
router.register(r"artists", views.ArtistViewSet, "artists") router.register(r"artists", views.ArtistViewSet, "artists")
router.register(r"channels", audio_views.ChannelViewSet, "channels")
router.register(r"subscriptions", audio_views.SubscriptionsViewSet, "subscriptions")
router.register(r"albums", views.AlbumViewSet, "albums") router.register(r"albums", views.AlbumViewSet, "albums")
router.register(r"import-batches", views.ImportBatchViewSet, "import-batches") router.register(r"licenses", views.LicenseViewSet, "licenses")
router.register(r"import-jobs", views.ImportJobViewSet, "import-jobs")
router.register(r"submit", views.SubmitViewSet, "submit")
router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists") router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists")
router.register( router.register(r"mutations", common_views.MutationViewSet, "mutations")
r"playlist-tracks", playlists_views.PlaylistTrackViewSet, "playlist-tracks" router.register(r"attachments", common_views.AttachmentViewSet, "attachments")
)
v1_patterns = router.urls v1_patterns = router.urls
subsonic_router = routers.SimpleRouter(trailing_slash=False)
subsonic_router.register(r"subsonic/rest", SubsonicViewSet, base_name="subsonic")
v1_patterns += [ v1_patterns += [
url( re_path(r"^oembed/$", views.OembedView.as_view(), name="oembed"),
re_path(
r"^instance/", r"^instance/",
include(("funkwhale_api.instance.urls", "instance"), namespace="instance"), include(("funkwhale_api.instance.urls", "instance"), namespace="instance"),
), ),
url( re_path(
r"^manage/", r"^manage/",
include(("funkwhale_api.manage.urls", "manage"), namespace="manage"), include(("funkwhale_api.manage.urls", "manage"), namespace="manage"),
), ),
url( re_path(
r"^moderation/",
include(
("funkwhale_api.moderation.urls", "moderation"), namespace="moderation"
),
),
re_path(
r"^federation/", r"^federation/",
include( include(
("funkwhale_api.federation.api_urls", "federation"), namespace="federation" ("funkwhale_api.federation.api_urls", "federation"), namespace="federation"
), ),
), ),
url( re_path(
r"^providers/", r"^providers/",
include(("funkwhale_api.providers.urls", "providers"), namespace="providers"), include(("funkwhale_api.providers.urls", "providers"), namespace="providers"),
), ),
url( re_path(
r"^favorites/", r"^favorites/",
include(("funkwhale_api.favorites.urls", "favorites"), namespace="favorites"), include(("funkwhale_api.favorites.urls", "favorites"), namespace="favorites"),
), ),
url(r"^search$", views.Search.as_view(), name="search"), re_path(r"^search$", views.Search.as_view(), name="search"),
url( re_path(
r"^radios/", r"^radios/",
include(("funkwhale_api.radios.urls", "radios"), namespace="radios"), include(("funkwhale_api.radios.urls", "radios"), namespace="radios"),
), ),
url( re_path(
r"^history/", r"^history/",
include(("funkwhale_api.history.urls", "history"), namespace="history"), include(("funkwhale_api.history.urls", "history"), namespace="history"),
), ),
url( re_path(
r"^users/", r"^",
include(("funkwhale_api.users.api_urls", "users"), namespace="users"), include(("funkwhale_api.users.api_urls", "users"), namespace="users"),
), ),
url( # XXX: remove if Funkwhale 1.1
r"^requests/", re_path(
include(("funkwhale_api.requests.api_urls", "requests"), namespace="requests"), r"^users/",
include(("funkwhale_api.users.api_urls", "users"), namespace="users-nested"),
),
re_path(
r"^oauth/",
include(("funkwhale_api.users.oauth.urls", "oauth"), namespace="oauth"),
),
re_path(r"^rate-limit/?$", common_views.RateLimitView.as_view(), name="rate-limit"),
re_path(
r"^text-preview/?$", common_views.TextPreviewView.as_view(), name="text-preview"
), ),
url(r"^token/$", jwt_views.obtain_jwt_token, name="token"),
url(r"^token/refresh/$", jwt_views.refresh_jwt_token, name="token_refresh"),
] ]
urlpatterns = [ urlpatterns = [re_path("", include((v1_patterns, "v1"), namespace="v1"))]
url(r"^v1/", include((v1_patterns, "v1"), namespace="v1"))
] + format_suffix_patterns(subsonic_router.urls, allowed=["view"])
from django.conf.urls import include
from django.urls import re_path
from funkwhale_api.common import routers as common_routers
from . import api
router = common_routers.OptionalSlashRouter()
v2_patterns = router.urls
v2_patterns += [
re_path(
r"^instance/",
include(("funkwhale_api.instance.urls_v2", "instance"), namespace="instance"),
),
re_path(
r"^radios/",
include(("funkwhale_api.radios.urls_v2", "radios"), namespace="radios"),
),
]
v2_paths = {
pattern.pattern.regex.pattern
for pattern in v2_patterns
if hasattr(pattern.pattern, "regex")
}
filtered_v1_patterns = [
pattern
for pattern in api.v1_patterns
if pattern.pattern.regex.pattern not in v2_paths
]
v2_patterns += filtered_v1_patterns
urlpatterns = [re_path("", include((v2_patterns, "v2"), namespace="v2"))]
from django import urls
from funkwhale_api.audio import spa_views as audio_spa_views
from funkwhale_api.federation import spa_views as federation_spa_views
from funkwhale_api.music import spa_views
urlpatterns = [
urls.re_path(
r"^library/tracks/(?P<pk>\d+)/?$", spa_views.library_track, name="library_track"
),
urls.re_path(
r"^library/albums/(?P<pk>\d+)/?$", spa_views.library_album, name="library_album"
),
urls.re_path(
r"^library/artists/(?P<pk>\d+)/?$",
spa_views.library_artist,
name="library_artist",
),
urls.re_path(
r"^library/playlists/(?P<pk>\d+)/?$",
spa_views.library_playlist,
name="library_playlist",
),
urls.re_path(
r"^library/(?P<uuid>[0-9a-f-]+)/?$",
spa_views.library_library,
name="library_library",
),
urls.re_path(
r"^channels/(?P<uuid>[0-9a-f-]+)/?$",
audio_spa_views.channel_detail_uuid,
name="channel_detail",
),
urls.re_path(
r"^channels/(?P<username>[^/]+)/?$",
audio_spa_views.channel_detail_username,
name="channel_detail",
),
urls.re_path(
r"^@(?P<username>[^/]+)/?$",
federation_spa_views.actor_detail_username,
name="actor_detail",
),
]
from django.conf.urls import include
from django.urls import re_path
from rest_framework import routers
from rest_framework.urlpatterns import format_suffix_patterns
from funkwhale_api.subsonic.views import SubsonicViewSet
subsonic_router = routers.SimpleRouter(trailing_slash=False)
subsonic_router.register(r"rest", SubsonicViewSet, basename="subsonic")
subsonic_patterns = format_suffix_patterns(subsonic_router.urls, allowed=["view"])
urlpatterns = [
re_path("", include((subsonic_patterns, "subsonic"), namespace="subsonic"))
]
# urlpatterns = [
# url(
# r"^subsonic/rest/",
# include((subsonic_router.urls, "subsonic"), namespace="subsonic"),
# )
# ]
"""
WSGI config for funkwhale_api project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
"""
import os
from django.core.wsgi import get_wsgi_application
from whitenoise.django import DjangoWhiteNoise
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
application = get_wsgi_application()
# Use Whitenoise to serve static files
# See: https://whitenoise.readthedocs.org/
application = DjangoWhiteNoise(application)
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)
from funkwhale_api.users.models import User
u = User.objects.create(email="demo@demo.com", username="demo", is_staff=True)
u.set_password("demo")
u.subsonic_api_token = "demo"
u.save()
#! /bin/bash
echo "Loading demo data..."
python manage.py migrate --noinput
echo "Creating demo user..."
cat demo/demo-user.py | python manage.py shell -i python
echo "Importing demo tracks..."
python manage.py import_files "/music/**/*.ogg" --recursive --noinput --username demo
FROM python:3.6
ENV PYTHONUNBUFFERED 1
# Requirements have to be pulled and installed here, otherwise caching won't work
RUN echo 'deb http://httpredir.debian.org/debian/ jessie-backports main' > /etc/apt/sources.list.d/ffmpeg.list
COPY ./requirements.apt /requirements.apt
RUN apt-get update; \
grep "^[^#;]" requirements.apt | \
grep -Fv "python3-dev" | \
xargs apt-get install -y --no-install-recommends; \
rm -rf /usr/share/doc/* /usr/share/locale/*
RUN curl -L https://github.com/acoustid/chromaprint/releases/download/v1.4.2/chromaprint-fpcalc-1.4.2-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin --strip 1
RUN mkdir /requirements
COPY ./requirements/base.txt /requirements/base.txt
RUN pip install -r /requirements/base.txt
COPY ./requirements/local.txt /requirements/local.txt
RUN pip install -r /requirements/local.txt
COPY ./requirements/test.txt /requirements/test.txt
RUN pip install -r /requirements/test.txt
COPY . /app
WORKDIR /app
ENTRYPOINT ["compose/django/dev-entrypoint.sh"]
#!/bin/sh
set -eux
funkwhale-manage collectstatic --noinput
funkwhale-manage migrate
# shellcheck disable=SC2086
exec gunicorn config.asgi:application \
--workers "${FUNKWHALE_WEB_WORKERS-1}" \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:"${FUNKWHALE_API_PORT}" \
${GUNICORN_ARGS-}