Merge branch 'black' into 'develop'
Blacked the code See merge request funkwhale/funkwhale!241
This commit is contained in:
commit
e953468e69
|
@ -12,70 +12,70 @@ from dynamic_preferences.api.viewsets import GlobalPreferencesViewSet
|
|||
from dynamic_preferences.users.viewsets import UserPreferencesViewSet
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r'settings', GlobalPreferencesViewSet, base_name='settings')
|
||||
router.register(r'activity', activity_views.ActivityViewSet, 'activity')
|
||||
router.register(r'tags', views.TagViewSet, 'tags')
|
||||
router.register(r'tracks', views.TrackViewSet, 'tracks')
|
||||
router.register(r'trackfiles', views.TrackFileViewSet, 'trackfiles')
|
||||
router.register(r'artists', views.ArtistViewSet, 'artists')
|
||||
router.register(r'albums', views.AlbumViewSet, 'albums')
|
||||
router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches')
|
||||
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"settings", GlobalPreferencesViewSet, base_name="settings")
|
||||
router.register(r"activity", activity_views.ActivityViewSet, "activity")
|
||||
router.register(r"tags", views.TagViewSet, "tags")
|
||||
router.register(r"tracks", views.TrackViewSet, "tracks")
|
||||
router.register(r"trackfiles", views.TrackFileViewSet, "trackfiles")
|
||||
router.register(r"artists", views.ArtistViewSet, "artists")
|
||||
router.register(r"albums", views.AlbumViewSet, "albums")
|
||||
router.register(r"import-batches", views.ImportBatchViewSet, "import-batches")
|
||||
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'playlist-tracks',
|
||||
playlists_views.PlaylistTrackViewSet,
|
||||
'playlist-tracks')
|
||||
r"playlist-tracks", playlists_views.PlaylistTrackViewSet, "playlist-tracks"
|
||||
)
|
||||
v1_patterns = router.urls
|
||||
|
||||
subsonic_router = routers.SimpleRouter(trailing_slash=False)
|
||||
subsonic_router.register(r'subsonic/rest', SubsonicViewSet, base_name='subsonic')
|
||||
subsonic_router.register(r"subsonic/rest", SubsonicViewSet, base_name="subsonic")
|
||||
|
||||
|
||||
v1_patterns += [
|
||||
url(r'^instance/',
|
||||
url(
|
||||
r"^instance/",
|
||||
include(("funkwhale_api.instance.urls", "instance"), namespace="instance"),
|
||||
),
|
||||
url(
|
||||
r"^manage/",
|
||||
include(("funkwhale_api.manage.urls", "manage"), namespace="manage"),
|
||||
),
|
||||
url(
|
||||
r"^federation/",
|
||||
include(
|
||||
('funkwhale_api.instance.urls', 'instance'),
|
||||
namespace='instance')),
|
||||
url(r'^manage/',
|
||||
include(
|
||||
('funkwhale_api.manage.urls', 'manage'),
|
||||
namespace='manage')),
|
||||
url(r'^federation/',
|
||||
include(
|
||||
('funkwhale_api.federation.api_urls', 'federation'),
|
||||
namespace='federation')),
|
||||
url(r'^providers/',
|
||||
include(
|
||||
('funkwhale_api.providers.urls', 'providers'),
|
||||
namespace='providers')),
|
||||
url(r'^favorites/',
|
||||
include(
|
||||
('funkwhale_api.favorites.urls', 'favorites'),
|
||||
namespace='favorites')),
|
||||
url(r'^search$',
|
||||
views.Search.as_view(), name='search'),
|
||||
url(r'^radios/',
|
||||
include(
|
||||
('funkwhale_api.radios.urls', 'radios'),
|
||||
namespace='radios')),
|
||||
url(r'^history/',
|
||||
include(
|
||||
('funkwhale_api.history.urls', 'history'),
|
||||
namespace='history')),
|
||||
url(r'^users/',
|
||||
include(
|
||||
('funkwhale_api.users.api_urls', 'users'),
|
||||
namespace='users')),
|
||||
url(r'^requests/',
|
||||
include(
|
||||
('funkwhale_api.requests.api_urls', 'requests'),
|
||||
namespace='requests')),
|
||||
url(r'^token/$', jwt_views.obtain_jwt_token, name='token'),
|
||||
url(r'^token/refresh/$', jwt_views.refresh_jwt_token, name='token_refresh'),
|
||||
("funkwhale_api.federation.api_urls", "federation"), namespace="federation"
|
||||
),
|
||||
),
|
||||
url(
|
||||
r"^providers/",
|
||||
include(("funkwhale_api.providers.urls", "providers"), namespace="providers"),
|
||||
),
|
||||
url(
|
||||
r"^favorites/",
|
||||
include(("funkwhale_api.favorites.urls", "favorites"), namespace="favorites"),
|
||||
),
|
||||
url(r"^search$", views.Search.as_view(), name="search"),
|
||||
url(
|
||||
r"^radios/",
|
||||
include(("funkwhale_api.radios.urls", "radios"), namespace="radios"),
|
||||
),
|
||||
url(
|
||||
r"^history/",
|
||||
include(("funkwhale_api.history.urls", "history"), namespace="history"),
|
||||
),
|
||||
url(
|
||||
r"^users/",
|
||||
include(("funkwhale_api.users.api_urls", "users"), namespace="users"),
|
||||
),
|
||||
url(
|
||||
r"^requests/",
|
||||
include(("funkwhale_api.requests.api_urls", "requests"), namespace="requests"),
|
||||
),
|
||||
url(r"^token/$", jwt_views.obtain_jwt_token, name="token"),
|
||||
url(r"^token/refresh/$", jwt_views.refresh_jwt_token, name="token_refresh"),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^v1/', include((v1_patterns, 'v1'), namespace='v1'))
|
||||
] + format_suffix_patterns(subsonic_router.urls, allowed=['view'])
|
||||
url(r"^v1/", include((v1_patterns, "v1"), namespace="v1"))
|
||||
] + format_suffix_patterns(subsonic_router.urls, allowed=["view"])
|
||||
|
|
|
@ -7,12 +7,13 @@ from funkwhale_api.common.auth import TokenAuthMiddleware
|
|||
from funkwhale_api.instance import consumers
|
||||
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
# Empty for now (http->django views is added by default)
|
||||
"websocket": TokenAuthMiddleware(
|
||||
URLRouter([
|
||||
url("^api/v1/instance/activity$",
|
||||
consumers.InstanceActivityConsumer),
|
||||
])
|
||||
),
|
||||
})
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
# Empty for now (http->django views is added by default)
|
||||
"websocket": TokenAuthMiddleware(
|
||||
URLRouter(
|
||||
[url("^api/v1/instance/activity$", consumers.InstanceActivityConsumer)]
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -18,123 +18,117 @@ from celery.schedules import crontab
|
|||
from funkwhale_api import __version__
|
||||
|
||||
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()
|
||||
|
||||
try:
|
||||
env.read_env(ROOT_DIR.file('.env'))
|
||||
env.read_env(ROOT_DIR.file(".env"))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
FUNKWHALE_HOSTNAME = None
|
||||
FUNKWHALE_HOSTNAME_SUFFIX = env('FUNKWHALE_HOSTNAME_SUFFIX', default=None)
|
||||
FUNKWHALE_HOSTNAME_PREFIX = env('FUNKWHALE_HOSTNAME_PREFIX', default=None)
|
||||
FUNKWHALE_HOSTNAME_SUFFIX = env("FUNKWHALE_HOSTNAME_SUFFIX", default=None)
|
||||
FUNKWHALE_HOSTNAME_PREFIX = env("FUNKWHALE_HOSTNAME_PREFIX", default=None)
|
||||
if FUNKWHALE_HOSTNAME_PREFIX and FUNKWHALE_HOSTNAME_SUFFIX:
|
||||
# We're in traefik case, in development
|
||||
FUNKWHALE_HOSTNAME = '{}.{}'.format(
|
||||
FUNKWHALE_HOSTNAME_PREFIX, FUNKWHALE_HOSTNAME_SUFFIX)
|
||||
FUNKWHALE_PROTOCOL = env('FUNKWHALE_PROTOCOL', default='https')
|
||||
FUNKWHALE_HOSTNAME = "{}.{}".format(
|
||||
FUNKWHALE_HOSTNAME_PREFIX, FUNKWHALE_HOSTNAME_SUFFIX
|
||||
)
|
||||
FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
|
||||
else:
|
||||
try:
|
||||
FUNKWHALE_HOSTNAME = env('FUNKWHALE_HOSTNAME')
|
||||
FUNKWHALE_PROTOCOL = env('FUNKWHALE_PROTOCOL', default='https')
|
||||
FUNKWHALE_HOSTNAME = env("FUNKWHALE_HOSTNAME")
|
||||
FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
|
||||
except Exception:
|
||||
FUNKWHALE_URL = env('FUNKWHALE_URL')
|
||||
FUNKWHALE_URL = env("FUNKWHALE_URL")
|
||||
_parsed = urlsplit(FUNKWHALE_URL)
|
||||
FUNKWHALE_HOSTNAME = _parsed.netloc
|
||||
FUNKWHALE_PROTOCOL = _parsed.scheme
|
||||
|
||||
FUNKWHALE_URL = '{}://{}'.format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME)
|
||||
FUNKWHALE_URL = "{}://{}".format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME)
|
||||
|
||||
|
||||
# XXX: deprecated, see #186
|
||||
FEDERATION_ENABLED = env.bool('FEDERATION_ENABLED', default=True)
|
||||
FEDERATION_HOSTNAME = env('FEDERATION_HOSTNAME', default=FUNKWHALE_HOSTNAME)
|
||||
FEDERATION_ENABLED = env.bool("FEDERATION_ENABLED", default=True)
|
||||
FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME)
|
||||
# XXX: deprecated, see #186
|
||||
FEDERATION_COLLECTION_PAGE_SIZE = env.int(
|
||||
'FEDERATION_COLLECTION_PAGE_SIZE', default=50
|
||||
)
|
||||
FEDERATION_COLLECTION_PAGE_SIZE = env.int("FEDERATION_COLLECTION_PAGE_SIZE", default=50)
|
||||
# XXX: deprecated, see #186
|
||||
FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool(
|
||||
'FEDERATION_MUSIC_NEEDS_APPROVAL', default=True
|
||||
"FEDERATION_MUSIC_NEEDS_APPROVAL", default=True
|
||||
)
|
||||
# XXX: deprecated, see #186
|
||||
FEDERATION_ACTOR_FETCH_DELAY = env.int(
|
||||
'FEDERATION_ACTOR_FETCH_DELAY', default=60 * 12)
|
||||
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
|
||||
FEDERATION_ACTOR_FETCH_DELAY = env.int("FEDERATION_ACTOR_FETCH_DELAY", default=60 * 12)
|
||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS")
|
||||
|
||||
# APP CONFIGURATION
|
||||
# ------------------------------------------------------------------------------
|
||||
DJANGO_APPS = (
|
||||
'channels',
|
||||
"channels",
|
||||
# Default Django apps:
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.postgres',
|
||||
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.postgres",
|
||||
# Useful template tags:
|
||||
# 'django.contrib.humanize',
|
||||
|
||||
# Admin
|
||||
'django.contrib.admin',
|
||||
"django.contrib.admin",
|
||||
)
|
||||
THIRD_PARTY_APPS = (
|
||||
# 'crispy_forms', # Form layouts
|
||||
'allauth', # registration
|
||||
'allauth.account', # registration
|
||||
'allauth.socialaccount', # registration
|
||||
'corsheaders',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'taggit',
|
||||
'rest_auth',
|
||||
'rest_auth.registration',
|
||||
'dynamic_preferences',
|
||||
'django_filters',
|
||||
'cacheops',
|
||||
'django_cleanup',
|
||||
"allauth", # registration
|
||||
"allauth.account", # registration
|
||||
"allauth.socialaccount", # registration
|
||||
"corsheaders",
|
||||
"rest_framework",
|
||||
"rest_framework.authtoken",
|
||||
"taggit",
|
||||
"rest_auth",
|
||||
"rest_auth.registration",
|
||||
"dynamic_preferences",
|
||||
"django_filters",
|
||||
"cacheops",
|
||||
"django_cleanup",
|
||||
)
|
||||
|
||||
|
||||
# Sentry
|
||||
RAVEN_ENABLED = env.bool("RAVEN_ENABLED", default=False)
|
||||
RAVEN_DSN = env("RAVEN_DSN", default='')
|
||||
RAVEN_DSN = env("RAVEN_DSN", default="")
|
||||
|
||||
if RAVEN_ENABLED:
|
||||
RAVEN_CONFIG = {
|
||||
'dsn': RAVEN_DSN,
|
||||
"dsn": RAVEN_DSN,
|
||||
# If you are using git, you can also automatically configure the
|
||||
# release based on the git info.
|
||||
'release': __version__,
|
||||
"release": __version__,
|
||||
}
|
||||
THIRD_PARTY_APPS += (
|
||||
'raven.contrib.django.raven_compat',
|
||||
)
|
||||
THIRD_PARTY_APPS += ("raven.contrib.django.raven_compat",)
|
||||
|
||||
|
||||
# Apps specific for this project go here.
|
||||
LOCAL_APPS = (
|
||||
'funkwhale_api.common',
|
||||
'funkwhale_api.activity.apps.ActivityConfig',
|
||||
'funkwhale_api.users', # custom users app
|
||||
"funkwhale_api.common",
|
||||
"funkwhale_api.activity.apps.ActivityConfig",
|
||||
"funkwhale_api.users", # custom users app
|
||||
# Your stuff: custom apps go here
|
||||
'funkwhale_api.instance',
|
||||
'funkwhale_api.music',
|
||||
'funkwhale_api.requests',
|
||||
'funkwhale_api.favorites',
|
||||
'funkwhale_api.federation',
|
||||
'funkwhale_api.radios',
|
||||
'funkwhale_api.history',
|
||||
'funkwhale_api.playlists',
|
||||
'funkwhale_api.providers.audiofile',
|
||||
'funkwhale_api.providers.youtube',
|
||||
'funkwhale_api.providers.acoustid',
|
||||
'funkwhale_api.subsonic',
|
||||
"funkwhale_api.instance",
|
||||
"funkwhale_api.music",
|
||||
"funkwhale_api.requests",
|
||||
"funkwhale_api.favorites",
|
||||
"funkwhale_api.federation",
|
||||
"funkwhale_api.radios",
|
||||
"funkwhale_api.history",
|
||||
"funkwhale_api.playlists",
|
||||
"funkwhale_api.providers.audiofile",
|
||||
"funkwhale_api.providers.youtube",
|
||||
"funkwhale_api.providers.acoustid",
|
||||
"funkwhale_api.subsonic",
|
||||
)
|
||||
|
||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
|
@ -145,20 +139,18 @@ INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
|||
# ------------------------------------------------------------------------------
|
||||
MIDDLEWARE = (
|
||||
# Make sure djangosecure.middleware.SecurityMiddleware is listed first
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
)
|
||||
|
||||
# MIGRATIONS CONFIGURATION
|
||||
# ------------------------------------------------------------------------------
|
||||
MIGRATION_MODULES = {
|
||||
'sites': 'funkwhale_api.contrib.sites.migrations'
|
||||
}
|
||||
MIGRATION_MODULES = {"sites": "funkwhale_api.contrib.sites.migrations"}
|
||||
|
||||
# DEBUG
|
||||
# ------------------------------------------------------------------------------
|
||||
|
@ -168,9 +160,7 @@ DEBUG = env.bool("DJANGO_DEBUG", False)
|
|||
# FIXTURE CONFIGURATION
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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
|
||||
# ------------------------------------------------------------------------------
|
||||
|
@ -178,16 +168,14 @@ FIXTURE_DIRS = (
|
|||
# EMAIL
|
||||
# ------------------------------------------------------------------------------
|
||||
DEFAULT_FROM_EMAIL = env(
|
||||
'DEFAULT_FROM_EMAIL',
|
||||
default='Funkwhale <noreply@{}>'.format(FUNKWHALE_HOSTNAME))
|
||||
"DEFAULT_FROM_EMAIL", default="Funkwhale <noreply@{}>".format(FUNKWHALE_HOSTNAME)
|
||||
)
|
||||
|
||||
EMAIL_SUBJECT_PREFIX = env(
|
||||
"EMAIL_SUBJECT_PREFIX", default='[Funkwhale] ')
|
||||
SERVER_EMAIL = env('SERVER_EMAIL', default=DEFAULT_FROM_EMAIL)
|
||||
EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Funkwhale] ")
|
||||
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://")
|
||||
|
||||
vars().update(EMAIL_CONFIG)
|
||||
|
||||
|
@ -196,9 +184,9 @@ vars().update(EMAIL_CONFIG)
|
|||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
|
||||
DATABASES = {
|
||||
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
|
||||
'default': env.db("DATABASE_URL"),
|
||||
"default": env.db("DATABASE_URL")
|
||||
}
|
||||
DATABASES['default']['ATOMIC_REQUESTS'] = True
|
||||
DATABASES["default"]["ATOMIC_REQUESTS"] = True
|
||||
#
|
||||
# DATABASES = {
|
||||
# 'default': {
|
||||
|
@ -212,10 +200,10 @@ DATABASES['default']['ATOMIC_REQUESTS'] = True
|
|||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
# In a Windows environment this must be set to your system time zone.
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
|
||||
SITE_ID = 1
|
||||
|
@ -235,126 +223,120 @@ USE_TZ = True
|
|||
TEMPLATES = [
|
||||
{
|
||||
# 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
|
||||
'DIRS': [
|
||||
str(APPS_DIR.path('templates')),
|
||||
],
|
||||
'OPTIONS': {
|
||||
"DIRS": [str(APPS_DIR.path("templates"))],
|
||||
"OPTIONS": {
|
||||
# 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
|
||||
# https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
|
||||
'loaders': [
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
"loaders": [
|
||||
"django.template.loaders.filesystem.Loader",
|
||||
"django.template.loaders.app_directories.Loader",
|
||||
],
|
||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.template.context_processors.i18n',
|
||||
'django.template.context_processors.media',
|
||||
'django.template.context_processors.static',
|
||||
'django.template.context_processors.tz',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.template.context_processors.i18n",
|
||||
"django.template.context_processors.media",
|
||||
"django.template.context_processors.static",
|
||||
"django.template.context_processors.tz",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
# Your stuff: custom template context processors go here
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
# See: http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap3'
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap3"
|
||||
|
||||
# STATIC FILE CONFIGURATION
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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")))
|
||||
|
||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
|
||||
STATIC_URL = env("STATIC_URL", default='/staticfiles/')
|
||||
DEFAULT_FILE_STORAGE = 'funkwhale_api.common.storage.ASCIIFileSystemStorage'
|
||||
STATIC_URL = env("STATIC_URL", default="/staticfiles/")
|
||||
DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIFileSystemStorage"
|
||||
|
||||
# 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
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||
)
|
||||
|
||||
# MEDIA CONFIGURATION
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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")))
|
||||
|
||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
|
||||
MEDIA_URL = env("MEDIA_URL", default='/media/')
|
||||
MEDIA_URL = env("MEDIA_URL", default="/media/")
|
||||
|
||||
# URL Configuration
|
||||
# ------------------------------------------------------------------------------
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
ROOT_URLCONF = "config.urls"
|
||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
ASGI_APPLICATION = "config.routing.application"
|
||||
|
||||
# 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")
|
||||
|
||||
# AUTHENTICATION CONFIGURATION
|
||||
# ------------------------------------------------------------------------------
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'allauth.account.auth_backends.AuthenticationBackend',
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"allauth.account.auth_backends.AuthenticationBackend",
|
||||
)
|
||||
SESSION_COOKIE_HTTPONLY = False
|
||||
# Some really nice defaults
|
||||
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
|
||||
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
|
||||
ACCOUNT_EMAIL_REQUIRED = True
|
||||
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
|
||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
|
||||
# Custom user app defaults
|
||||
# Select the correct user model
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
LOGIN_REDIRECT_URL = 'users:redirect'
|
||||
LOGIN_URL = 'account_login'
|
||||
AUTH_USER_MODEL = "users.User"
|
||||
LOGIN_REDIRECT_URL = "users:redirect"
|
||||
LOGIN_URL = "account_login"
|
||||
|
||||
# SLUGLIFIER
|
||||
AUTOSLUG_SLUGIFY_FUNCTION = 'slugify.slugify'
|
||||
AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify"
|
||||
|
||||
CACHE_DEFAULT = "redis://127.0.0.1:6379/0"
|
||||
CACHES = {
|
||||
"default": env.cache_url('CACHE_URL', default=CACHE_DEFAULT)
|
||||
}
|
||||
CACHES = {"default": env.cache_url("CACHE_URL", default=CACHE_DEFAULT)}
|
||||
|
||||
CACHES["default"]["BACKEND"] = "django_redis.cache.RedisCache"
|
||||
from urllib.parse import urlparse
|
||||
cache_url = urlparse(CACHES['default']['LOCATION'])
|
||||
|
||||
cache_url = urlparse(CACHES["default"]["LOCATION"])
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(cache_url.hostname, cache_url.port)],
|
||||
},
|
||||
},
|
||||
"CONFIG": {"hosts": [(cache_url.hostname, cache_url.port)]},
|
||||
}
|
||||
}
|
||||
|
||||
CACHES["default"]["OPTIONS"] = {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"IGNORE_EXCEPTIONS": True, # mimics memcache behavior.
|
||||
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
|
||||
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
|
||||
}
|
||||
|
||||
|
||||
########## CELERY
|
||||
INSTALLED_APPS += ('funkwhale_api.taskapp.celery.CeleryConfig',)
|
||||
INSTALLED_APPS += ("funkwhale_api.taskapp.celery.CeleryConfig",)
|
||||
CELERY_BROKER_URL = env(
|
||||
"CELERY_BROKER_URL", default=env('CACHE_URL', default=CACHE_DEFAULT))
|
||||
"CELERY_BROKER_URL", default=env("CACHE_URL", default=CACHE_DEFAULT)
|
||||
)
|
||||
########## END CELERY
|
||||
# Location of root django.contrib.admin URL, use {% url 'admin:index' %}
|
||||
|
||||
|
@ -362,25 +344,24 @@ CELERY_BROKER_URL = env(
|
|||
CELERY_TASK_DEFAULT_RATE_LIMIT = 1
|
||||
CELERY_TASK_TIME_LIMIT = 300
|
||||
CELERYBEAT_SCHEDULE = {
|
||||
'federation.clean_music_cache': {
|
||||
'task': 'funkwhale_api.federation.tasks.clean_music_cache',
|
||||
'schedule': crontab(hour='*/2'),
|
||||
'options': {
|
||||
'expires': 60 * 2,
|
||||
},
|
||||
"federation.clean_music_cache": {
|
||||
"task": "funkwhale_api.federation.tasks.clean_music_cache",
|
||||
"schedule": crontab(hour="*/2"),
|
||||
"options": {"expires": 60 * 2},
|
||||
}
|
||||
}
|
||||
|
||||
import datetime
|
||||
|
||||
JWT_AUTH = {
|
||||
'JWT_ALLOW_REFRESH': True,
|
||||
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
|
||||
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=30),
|
||||
'JWT_AUTH_HEADER_PREFIX': 'JWT',
|
||||
'JWT_GET_USER_SECRET_KEY': lambda user: user.secret_key
|
||||
"JWT_ALLOW_REFRESH": True,
|
||||
"JWT_EXPIRATION_DELTA": datetime.timedelta(days=7),
|
||||
"JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=30),
|
||||
"JWT_AUTH_HEADER_PREFIX": "JWT",
|
||||
"JWT_GET_USER_SECRET_KEY": lambda user: user.secret_key,
|
||||
}
|
||||
OLD_PASSWORD_FIELD_ENABLED = True
|
||||
ACCOUNT_ADAPTER = 'funkwhale_api.users.adapters.FunkwhaleAccountAdapter'
|
||||
ACCOUNT_ADAPTER = "funkwhale_api.users.adapters.FunkwhaleAccountAdapter"
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
# CORS_ORIGIN_WHITELIST = (
|
||||
# 'localhost',
|
||||
|
@ -389,41 +370,37 @@ CORS_ORIGIN_ALLOW_ALL = True
|
|||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
|
||||
"DEFAULT_PAGINATION_CLASS": "funkwhale_api.common.pagination.FunkwhalePagination",
|
||||
"PAGE_SIZE": 25,
|
||||
"DEFAULT_PARSER_CLASSES": (
|
||||
"rest_framework.parsers.JSONParser",
|
||||
"rest_framework.parsers.FormParser",
|
||||
"rest_framework.parsers.MultiPartParser",
|
||||
"funkwhale_api.federation.parsers.ActivityParser",
|
||||
),
|
||||
'DEFAULT_PAGINATION_CLASS': 'funkwhale_api.common.pagination.FunkwhalePagination',
|
||||
'PAGE_SIZE': 25,
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
'rest_framework.parsers.JSONParser',
|
||||
'rest_framework.parsers.FormParser',
|
||||
'rest_framework.parsers.MultiPartParser',
|
||||
'funkwhale_api.federation.parsers.ActivityParser',
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS",
|
||||
"funkwhale_api.common.authentication.BearerTokenHeaderAuth",
|
||||
"rest_framework_jwt.authentication.JSONWebTokenAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
"rest_framework.authentication.BasicAuthentication",
|
||||
),
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS',
|
||||
'funkwhale_api.common.authentication.BearerTokenHeaderAuth',
|
||||
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
"DEFAULT_FILTER_BACKENDS": (
|
||||
"rest_framework.filters.OrderingFilter",
|
||||
"django_filters.rest_framework.DjangoFilterBackend",
|
||||
),
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'rest_framework.filters.OrderingFilter',
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
),
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
)
|
||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||
}
|
||||
|
||||
BROWSABLE_API_ENABLED = env.bool('BROWSABLE_API_ENABLED', default=False)
|
||||
BROWSABLE_API_ENABLED = env.bool("BROWSABLE_API_ENABLED", default=False)
|
||||
if BROWSABLE_API_ENABLED:
|
||||
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] += (
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] += (
|
||||
"rest_framework.renderers.BrowsableAPIRenderer",
|
||||
)
|
||||
|
||||
REST_AUTH_SERIALIZERS = {
|
||||
'PASSWORD_RESET_SERIALIZER': 'funkwhale_api.users.serializers.PasswordResetSerializer' # noqa
|
||||
"PASSWORD_RESET_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetSerializer" # noqa
|
||||
}
|
||||
REST_SESSION_LOGIN = False
|
||||
REST_USE_JWT = True
|
||||
|
@ -434,60 +411,55 @@ USE_X_FORWARDED_PORT = True
|
|||
|
||||
# Wether we should use Apache, Nginx (or other) headers when serving audio files
|
||||
# Default to Nginx
|
||||
REVERSE_PROXY_TYPE = env('REVERSE_PROXY_TYPE', default='nginx')
|
||||
assert REVERSE_PROXY_TYPE in ['apache2', 'nginx'], 'Unsupported REVERSE_PROXY_TYPE'
|
||||
REVERSE_PROXY_TYPE = env("REVERSE_PROXY_TYPE", default="nginx")
|
||||
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")
|
||||
|
||||
|
||||
# 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
|
||||
)
|
||||
CACHEOPS_REDIS = env('CACHE_URL', default=CACHE_DEFAULT)
|
||||
CACHEOPS_ENABLED = env.bool('CACHEOPS_ENABLED', default=True)
|
||||
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)
|
||||
CACHEOPS = {
|
||||
'music.artist': {'ops': 'all', 'timeout': 60 * 60},
|
||||
'music.album': {'ops': 'all', 'timeout': 60 * 60},
|
||||
'music.track': {'ops': 'all', 'timeout': 60 * 60},
|
||||
'music.trackfile': {'ops': 'all', 'timeout': 60 * 60},
|
||||
'taggit.tag': {'ops': 'all', 'timeout': 60 * 60},
|
||||
"music.artist": {"ops": "all", "timeout": 60 * 60},
|
||||
"music.album": {"ops": "all", "timeout": 60 * 60},
|
||||
"music.track": {"ops": "all", "timeout": 60 * 60},
|
||||
"music.trackfile": {"ops": "all", "timeout": 60 * 60},
|
||||
"taggit.tag": {"ops": "all", "timeout": 60 * 60},
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
# Playlist settings
|
||||
# XXX: deprecated, see #186
|
||||
PLAYLISTS_MAX_TRACKS = env.int('PLAYLISTS_MAX_TRACKS', default=250)
|
||||
PLAYLISTS_MAX_TRACKS = env.int("PLAYLISTS_MAX_TRACKS", default=250)
|
||||
|
||||
ACCOUNT_USERNAME_BLACKLIST = [
|
||||
'funkwhale',
|
||||
'library',
|
||||
'test',
|
||||
'status',
|
||||
'root',
|
||||
'admin',
|
||||
'owner',
|
||||
'superuser',
|
||||
'staff',
|
||||
'service',
|
||||
] + env.list('ACCOUNT_USERNAME_BLACKLIST', default=[])
|
||||
"funkwhale",
|
||||
"library",
|
||||
"test",
|
||||
"status",
|
||||
"root",
|
||||
"admin",
|
||||
"owner",
|
||||
"superuser",
|
||||
"staff",
|
||||
"service",
|
||||
] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[])
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
MUSIC_DIRECTORY_SERVE_PATH = env(
|
||||
'MUSIC_DIRECTORY_SERVE_PATH', default=MUSIC_DIRECTORY_PATH)
|
||||
"MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH
|
||||
)
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Local settings
|
||||
|
||||
- Run in Debug mode
|
||||
- Use console backend for emails
|
||||
- Add Django Debug Toolbar
|
||||
- Add django-extensions as app
|
||||
'''
|
||||
"""
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
# DEBUG
|
||||
# ------------------------------------------------------------------------------
|
||||
DEBUG = env.bool('DJANGO_DEBUG', default=True)
|
||||
TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
|
||||
DEBUG = env.bool("DJANGO_DEBUG", default=True)
|
||||
TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG
|
||||
|
||||
# SECRET CONFIGURATION
|
||||
# ------------------------------------------------------------------------------
|
||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
|
||||
# Note: This key only used for development and testing.
|
||||
SECRET_KEY = env("DJANGO_SECRET_KEY", default='mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc')
|
||||
SECRET_KEY = env(
|
||||
"DJANGO_SECRET_KEY", default="mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc"
|
||||
)
|
||||
|
||||
# Mail settings
|
||||
# ------------------------------------------------------------------------------
|
||||
EMAIL_HOST = 'localhost'
|
||||
EMAIL_HOST = "localhost"
|
||||
EMAIL_PORT = 1025
|
||||
|
||||
# django-debug-toolbar
|
||||
# ------------------------------------------------------------------------------
|
||||
MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
|
||||
MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",)
|
||||
|
||||
# INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',)
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
'DISABLE_PANELS': [
|
||||
'debug_toolbar.panels.redirects.RedirectsPanel',
|
||||
],
|
||||
'SHOW_TEMPLATE_CONTEXT': True,
|
||||
'SHOW_TOOLBAR_CALLBACK': lambda request: True,
|
||||
"DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
|
||||
"SHOW_TEMPLATE_CONTEXT": True,
|
||||
"SHOW_TOOLBAR_CALLBACK": lambda request: True,
|
||||
}
|
||||
|
||||
# django-extensions
|
||||
# ------------------------------------------------------------------------------
|
||||
# INSTALLED_APPS += ('django_extensions', )
|
||||
INSTALLED_APPS += ('debug_toolbar', )
|
||||
INSTALLED_APPS += ("debug_toolbar",)
|
||||
|
||||
# TESTING
|
||||
# ------------------------------------------------------------------------------
|
||||
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
|
||||
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
||||
|
||||
########## CELERY
|
||||
# In development, all tasks will be executed locally by blocking until the task returns
|
||||
|
@ -57,23 +57,15 @@ CELERY_TASK_ALWAYS_EAGER = False
|
|||
# Your local stuff: Below this line define 3rd party library settings
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'handlers': {
|
||||
'console':{
|
||||
'level':'DEBUG',
|
||||
'class':'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django.request': {
|
||||
'handlers':['console'],
|
||||
'propagate': True,
|
||||
'level':'DEBUG',
|
||||
},
|
||||
'': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console'],
|
||||
"version": 1,
|
||||
"handlers": {"console": {"level": "DEBUG", "class": "logging.StreamHandler"}},
|
||||
"loggers": {
|
||||
"django.request": {
|
||||
"handlers": ["console"],
|
||||
"propagate": True,
|
||||
"level": "DEBUG",
|
||||
},
|
||||
"": {"level": "DEBUG", "handlers": ["console"]},
|
||||
},
|
||||
}
|
||||
CSRF_TRUSTED_ORIGINS = [o for o in ALLOWED_HOSTS]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Production Configurations
|
||||
|
||||
- Use djangosecure
|
||||
|
@ -8,7 +8,7 @@ Production Configurations
|
|||
- Use Redis on Heroku
|
||||
|
||||
|
||||
'''
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils import six
|
||||
|
@ -58,19 +58,24 @@ CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
|||
# ------------------------------------------------------------------------------
|
||||
# Uploaded Media Files
|
||||
# ------------------------
|
||||
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
|
||||
|
||||
# Static Assets
|
||||
# ------------------------
|
||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
|
||||
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||
|
||||
# TEMPLATE CONFIGURATION
|
||||
# ------------------------------------------------------------------------------
|
||||
# See:
|
||||
# https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.loaders.cached.Loader
|
||||
TEMPLATES[0]['OPTIONS']['loaders'] = [
|
||||
('django.template.loaders.cached.Loader', [
|
||||
'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ]),
|
||||
TEMPLATES[0]["OPTIONS"]["loaders"] = [
|
||||
(
|
||||
"django.template.loaders.cached.Loader",
|
||||
[
|
||||
"django.template.loaders.filesystem.Loader",
|
||||
"django.template.loaders.app_directories.Loader",
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
# CACHING
|
||||
|
@ -78,7 +83,6 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [
|
|||
# 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
|
||||
|
@ -88,43 +92,39 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [
|
|||
# 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'
|
||||
"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"
|
||||
}
|
||||
},
|
||||
'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",
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'filters': ['require_debug_false'],
|
||||
'class': 'django.utils.log.AdminEmailHandler'
|
||||
"loggers": {
|
||||
"django.request": {
|
||||
"handlers": ["mail_admins"],
|
||||
"level": "ERROR",
|
||||
"propagate": True,
|
||||
},
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
"django.security.DisallowedHost": {
|
||||
"level": "ERROR",
|
||||
"handlers": ["console", "mail_admins"],
|
||||
"propagate": True,
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django.request': {
|
||||
'handlers': ['mail_admins'],
|
||||
'level': 'ERROR',
|
||||
'propagate': True
|
||||
},
|
||||
'django.security.DisallowedHost': {
|
||||
'level': 'ERROR',
|
||||
'handlers': ['console', 'mail_admins'],
|
||||
'propagate': True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -11,32 +11,30 @@ from django.views import defaults as default_views
|
|||
urlpatterns = [
|
||||
# Django Admin, use {% url 'admin:index' %}
|
||||
url(settings.ADMIN_URL, admin.site.urls),
|
||||
|
||||
url(r'^api/', include(("config.api_urls", 'api'), namespace="api")),
|
||||
url(r'^', include(
|
||||
('funkwhale_api.federation.urls', 'federation'),
|
||||
namespace="federation")),
|
||||
url(r'^api/v1/auth/', include('rest_auth.urls')),
|
||||
url(r'^api/v1/auth/registration/', include('funkwhale_api.users.rest_auth_urls')),
|
||||
url(r'^accounts/', include('allauth.urls')),
|
||||
|
||||
url(r"^api/", include(("config.api_urls", "api"), namespace="api")),
|
||||
url(
|
||||
r"^",
|
||||
include(
|
||||
("funkwhale_api.federation.urls", "federation"), namespace="federation"
|
||||
),
|
||||
),
|
||||
url(r"^api/v1/auth/", include("rest_auth.urls")),
|
||||
url(r"^api/v1/auth/registration/", include("funkwhale_api.users.rest_auth_urls")),
|
||||
url(r"^accounts/", include("allauth.urls")),
|
||||
# Your stuff: custom urls includes go here
|
||||
|
||||
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
# This allows the error pages to be debugged during development, just visit
|
||||
# these url in browser to see how these error pages look like.
|
||||
urlpatterns += [
|
||||
url(r'^400/$', default_views.bad_request),
|
||||
url(r'^403/$', default_views.permission_denied),
|
||||
url(r'^404/$', default_views.page_not_found),
|
||||
url(r'^500/$', default_views.server_error),
|
||||
url(r"^400/$", default_views.bad_request),
|
||||
url(r"^403/$", default_views.permission_denied),
|
||||
url(r"^404/$", default_views.page_not_found),
|
||||
url(r"^500/$", default_views.server_error),
|
||||
] + 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
|
||||
urlpatterns += [
|
||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
]
|
||||
|
||||
urlpatterns += [url(r"^__debug__/", include(debug_toolbar.urls))]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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 = User.objects.create(email="demo@demo.com", username="demo", is_staff=True)
|
||||
u.set_password("demo")
|
||||
u.subsonic_api_token = "demo"
|
||||
u.save()
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
__version__ = '0.14.1'
|
||||
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
|
||||
__version__ = "0.14.1"
|
||||
__version_info__ = tuple(
|
||||
[
|
||||
int(num) if num.isdigit() else num
|
||||
for num in __version__.replace("-", ".", 1).split(".")
|
||||
]
|
||||
)
|
||||
|
|
|
@ -2,8 +2,9 @@ from django.apps import AppConfig, apps
|
|||
|
||||
from . import record
|
||||
|
||||
|
||||
class ActivityConfig(AppConfig):
|
||||
name = 'funkwhale_api.activity'
|
||||
name = "funkwhale_api.activity"
|
||||
|
||||
def ready(self):
|
||||
super(ActivityConfig, self).ready()
|
||||
|
|
|
@ -2,37 +2,36 @@ import persisting_theory
|
|||
|
||||
|
||||
class ActivityRegistry(persisting_theory.Registry):
|
||||
look_into = 'activities'
|
||||
look_into = "activities"
|
||||
|
||||
def _register_for_model(self, model, attr, value):
|
||||
key = model._meta.label
|
||||
d = self.setdefault(key, {'consumers': []})
|
||||
d = self.setdefault(key, {"consumers": []})
|
||||
d[attr] = value
|
||||
|
||||
def register_serializer(self, serializer_class):
|
||||
model = serializer_class.Meta.model
|
||||
self._register_for_model(model, 'serializer', serializer_class)
|
||||
self._register_for_model(model, "serializer", serializer_class)
|
||||
return serializer_class
|
||||
|
||||
def register_consumer(self, label):
|
||||
def decorator(func):
|
||||
consumers = self[label]['consumers']
|
||||
consumers = self[label]["consumers"]
|
||||
if func not in consumers:
|
||||
consumers.append(func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
registry = ActivityRegistry()
|
||||
|
||||
|
||||
|
||||
|
||||
def send(obj):
|
||||
conf = registry[obj.__class__._meta.label]
|
||||
consumers = conf['consumers']
|
||||
consumers = conf["consumers"]
|
||||
if not consumers:
|
||||
return
|
||||
serializer = conf['serializer'](obj)
|
||||
serializer = conf["serializer"](obj)
|
||||
for consumer in consumers:
|
||||
consumer(data=serializer.data, obj=obj)
|
||||
|
|
|
@ -4,8 +4,8 @@ from funkwhale_api.activity import record
|
|||
|
||||
|
||||
class ModelSerializer(serializers.ModelSerializer):
|
||||
id = serializers.CharField(source='get_activity_url')
|
||||
local_id = serializers.IntegerField(source='id')
|
||||
id = serializers.CharField(source="get_activity_url")
|
||||
local_id = serializers.IntegerField(source="id")
|
||||
# url = serializers.SerializerMethodField()
|
||||
|
||||
def get_url(self, obj):
|
||||
|
@ -17,8 +17,7 @@ class AutoSerializer(serializers.Serializer):
|
|||
A serializer that will automatically use registered activity serializers
|
||||
to serialize an henerogeneous list of objects (favorites, listenings, etc.)
|
||||
"""
|
||||
|
||||
def to_representation(self, instance):
|
||||
serializer = record.registry[instance._meta.label]['serializer'](
|
||||
instance
|
||||
)
|
||||
serializer = record.registry[instance._meta.label]["serializer"](instance)
|
||||
return serializer.data
|
||||
|
|
|
@ -6,31 +6,25 @@ from funkwhale_api.history.models import Listening
|
|||
|
||||
|
||||
def combined_recent(limit, **kwargs):
|
||||
datetime_field = kwargs.pop('datetime_field', 'creation_date')
|
||||
source_querysets = {
|
||||
qs.model._meta.label: qs for qs in kwargs.pop('querysets')
|
||||
}
|
||||
datetime_field = kwargs.pop("datetime_field", "creation_date")
|
||||
source_querysets = {qs.model._meta.label: qs for qs in kwargs.pop("querysets")}
|
||||
querysets = {
|
||||
k: qs.annotate(
|
||||
__type=models.Value(
|
||||
qs.model._meta.label, output_field=models.CharField()
|
||||
)
|
||||
).values('pk', datetime_field, '__type')
|
||||
__type=models.Value(qs.model._meta.label, output_field=models.CharField())
|
||||
).values("pk", datetime_field, "__type")
|
||||
for k, qs in source_querysets.items()
|
||||
}
|
||||
_qs_list = list(querysets.values())
|
||||
union_qs = _qs_list[0].union(*_qs_list[1:])
|
||||
records = []
|
||||
for row in union_qs.order_by('-{}'.format(datetime_field))[:limit]:
|
||||
records.append({
|
||||
'type': row['__type'],
|
||||
'when': row[datetime_field],
|
||||
'pk': row['pk']
|
||||
})
|
||||
for row in union_qs.order_by("-{}".format(datetime_field))[:limit]:
|
||||
records.append(
|
||||
{"type": row["__type"], "when": row[datetime_field], "pk": row["pk"]}
|
||||
)
|
||||
# Now we bulk-load each object type in turn
|
||||
to_load = {}
|
||||
for record in records:
|
||||
to_load.setdefault(record['type'], []).append(record['pk'])
|
||||
to_load.setdefault(record["type"], []).append(record["pk"])
|
||||
fetched = {}
|
||||
|
||||
for key, pks in to_load.items():
|
||||
|
@ -39,26 +33,19 @@ def combined_recent(limit, **kwargs):
|
|||
|
||||
# Annotate 'records' with loaded objects
|
||||
for record in records:
|
||||
record['object'] = fetched[(record['type'], record['pk'])]
|
||||
record["object"] = fetched[(record["type"], record["pk"])]
|
||||
return records
|
||||
|
||||
|
||||
def get_activity(user, limit=20):
|
||||
query = fields.privacy_level_query(
|
||||
user, lookup_field='user__privacy_level')
|
||||
query = fields.privacy_level_query(user, lookup_field="user__privacy_level")
|
||||
querysets = [
|
||||
Listening.objects.filter(query).select_related(
|
||||
'track',
|
||||
'user',
|
||||
'track__artist',
|
||||
'track__album__artist',
|
||||
"track", "user", "track__artist", "track__album__artist"
|
||||
),
|
||||
TrackFavorite.objects.filter(query).select_related(
|
||||
'track',
|
||||
'user',
|
||||
'track__artist',
|
||||
'track__album__artist',
|
||||
"track", "user", "track__artist", "track__album__artist"
|
||||
),
|
||||
]
|
||||
records = combined_recent(limit=limit, querysets=querysets)
|
||||
return [r['object'] for r in records]
|
||||
return [r["object"] for r in records]
|
||||
|
|
|
@ -17,4 +17,4 @@ class ActivityViewSet(viewsets.GenericViewSet):
|
|||
def list(self, request, *args, **kwargs):
|
||||
activity = utils.get_activity(user=request.user)
|
||||
serializer = self.serializer_class(activity, many=True)
|
||||
return Response({'results': serializer.data}, status=200)
|
||||
return Response({"results": serializer.data}, status=200)
|
||||
|
|
|
@ -16,20 +16,19 @@ class TokenHeaderAuth(BaseJSONWebTokenAuthentication):
|
|||
def get_jwt_value(self, request):
|
||||
|
||||
try:
|
||||
qs = request.get('query_string', b'').decode('utf-8')
|
||||
qs = request.get("query_string", b"").decode("utf-8")
|
||||
parsed = parse_qs(qs)
|
||||
token = parsed['token'][0]
|
||||
token = parsed["token"][0]
|
||||
except KeyError:
|
||||
raise exceptions.AuthenticationFailed('No token')
|
||||
raise exceptions.AuthenticationFailed("No token")
|
||||
|
||||
if not token:
|
||||
raise exceptions.AuthenticationFailed('Empty token')
|
||||
raise exceptions.AuthenticationFailed("Empty token")
|
||||
|
||||
return token
|
||||
|
||||
|
||||
class TokenAuthMiddleware:
|
||||
|
||||
def __init__(self, inner):
|
||||
# Store the ASGI application we were passed
|
||||
self.inner = inner
|
||||
|
@ -41,5 +40,5 @@ class TokenAuthMiddleware:
|
|||
except (User.DoesNotExist, exceptions.AuthenticationFailed):
|
||||
user = AnonymousUser()
|
||||
|
||||
scope['user'] = user
|
||||
scope["user"] = user
|
||||
return self.inner(scope)
|
||||
|
|
|
@ -6,34 +6,34 @@ from rest_framework_jwt import authentication
|
|||
from rest_framework_jwt.settings import api_settings
|
||||
|
||||
|
||||
class JSONWebTokenAuthenticationQS(
|
||||
authentication.BaseJSONWebTokenAuthentication):
|
||||
class JSONWebTokenAuthenticationQS(authentication.BaseJSONWebTokenAuthentication):
|
||||
|
||||
www_authenticate_realm = 'api'
|
||||
www_authenticate_realm = "api"
|
||||
|
||||
def get_jwt_value(self, request):
|
||||
token = request.query_params.get('jwt')
|
||||
if 'jwt' in request.query_params and not token:
|
||||
msg = _('Invalid Authorization header. No credentials provided.')
|
||||
token = request.query_params.get("jwt")
|
||||
if "jwt" in request.query_params and not token:
|
||||
msg = _("Invalid Authorization header. No credentials provided.")
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
return token
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return '{0} realm="{1}"'.format(
|
||||
api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)
|
||||
api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm
|
||||
)
|
||||
|
||||
|
||||
class BearerTokenHeaderAuth(
|
||||
authentication.BaseJSONWebTokenAuthentication):
|
||||
class BearerTokenHeaderAuth(authentication.BaseJSONWebTokenAuthentication):
|
||||
"""
|
||||
For backward compatibility purpose, we used Authorization: JWT <token>
|
||||
but Authorization: Bearer <token> is probably better.
|
||||
"""
|
||||
www_authenticate_realm = 'api'
|
||||
|
||||
www_authenticate_realm = "api"
|
||||
|
||||
def get_jwt_value(self, request):
|
||||
auth = authentication.get_authorization_header(request).split()
|
||||
auth_header_prefix = 'bearer'
|
||||
auth_header_prefix = "bearer"
|
||||
|
||||
if not auth:
|
||||
if api_settings.JWT_AUTH_COOKIE:
|
||||
|
@ -44,14 +44,16 @@ class BearerTokenHeaderAuth(
|
|||
return None
|
||||
|
||||
if len(auth) == 1:
|
||||
msg = _('Invalid Authorization header. No credentials provided.')
|
||||
msg = _("Invalid Authorization header. No credentials provided.")
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
elif len(auth) > 2:
|
||||
msg = _('Invalid Authorization header. Credentials string '
|
||||
'should not contain spaces.')
|
||||
msg = _(
|
||||
"Invalid Authorization header. Credentials string "
|
||||
"should not contain spaces."
|
||||
)
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
return auth[1]
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return '{0} realm="{1}"'.format('Bearer', self.www_authenticate_realm)
|
||||
return '{0} realm="{1}"'.format("Bearer", self.www_authenticate_realm)
|
||||
|
|
|
@ -5,7 +5,7 @@ from funkwhale_api.common import channels
|
|||
class JsonAuthConsumer(JsonWebsocketConsumer):
|
||||
def connect(self):
|
||||
try:
|
||||
assert self.scope['user'].pk is not None
|
||||
assert self.scope["user"].pk is not None
|
||||
except (AssertionError, AttributeError, KeyError):
|
||||
return self.close()
|
||||
|
||||
|
|
|
@ -3,18 +3,19 @@ from dynamic_preferences.registries import global_preferences_registry
|
|||
|
||||
from funkwhale_api.common import preferences
|
||||
|
||||
common = types.Section('common')
|
||||
common = types.Section("common")
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class APIAutenticationRequired(
|
||||
preferences.DefaultFromSettingMixin, types.BooleanPreference):
|
||||
preferences.DefaultFromSettingMixin, types.BooleanPreference
|
||||
):
|
||||
section = common
|
||||
name = 'api_authentication_required'
|
||||
verbose_name = 'API Requires authentication'
|
||||
setting = 'API_AUTHENTICATION_REQUIRED'
|
||||
name = "api_authentication_required"
|
||||
verbose_name = "API Requires authentication"
|
||||
setting = "API_AUTHENTICATION_REQUIRED"
|
||||
help_text = (
|
||||
'If disabled, anonymous users will be able to query the API'
|
||||
'and access music data (as well as other data exposed in the API '
|
||||
'without specific permissions).'
|
||||
"If disabled, anonymous users will be able to query the API"
|
||||
"and access music data (as well as other data exposed in the API "
|
||||
"without specific permissions)."
|
||||
)
|
||||
|
|
|
@ -6,34 +6,31 @@ from funkwhale_api.music import utils
|
|||
|
||||
|
||||
PRIVACY_LEVEL_CHOICES = [
|
||||
('me', 'Only me'),
|
||||
('followers', 'Me and my followers'),
|
||||
('instance', 'Everyone on my instance, and my followers'),
|
||||
('everyone', 'Everyone, including people on other instances'),
|
||||
("me", "Only me"),
|
||||
("followers", "Me and my followers"),
|
||||
("instance", "Everyone on my instance, and my followers"),
|
||||
("everyone", "Everyone, including people on other instances"),
|
||||
]
|
||||
|
||||
|
||||
def get_privacy_field():
|
||||
return models.CharField(
|
||||
max_length=30, choices=PRIVACY_LEVEL_CHOICES, default='instance')
|
||||
max_length=30, choices=PRIVACY_LEVEL_CHOICES, default="instance"
|
||||
)
|
||||
|
||||
|
||||
def privacy_level_query(user, lookup_field='privacy_level'):
|
||||
def privacy_level_query(user, lookup_field="privacy_level"):
|
||||
if user.is_anonymous:
|
||||
return models.Q(**{
|
||||
lookup_field: 'everyone',
|
||||
})
|
||||
return models.Q(**{lookup_field: "everyone"})
|
||||
|
||||
return models.Q(**{
|
||||
'{}__in'.format(lookup_field): [
|
||||
'followers', 'instance', 'everyone'
|
||||
]
|
||||
})
|
||||
return models.Q(
|
||||
**{"{}__in".format(lookup_field): ["followers", "instance", "everyone"]}
|
||||
)
|
||||
|
||||
|
||||
class SearchFilter(django_filters.CharFilter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.search_fields = kwargs.pop('search_fields')
|
||||
self.search_fields = kwargs.pop("search_fields")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, qs, value):
|
||||
|
|
|
@ -4,17 +4,20 @@ from funkwhale_api.common import scripts
|
|||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Run a specific script from funkwhale_api/common/scripts/'
|
||||
help = "Run a specific script from funkwhale_api/common/scripts/"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('script_name', nargs='?', type=str)
|
||||
parser.add_argument("script_name", nargs="?", type=str)
|
||||
parser.add_argument(
|
||||
'--noinput', '--no-input', action='store_false', dest='interactive',
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
help="Do NOT prompt the user for input of any kind.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
name = options['script_name']
|
||||
name = options["script_name"]
|
||||
if not name:
|
||||
self.show_help()
|
||||
|
||||
|
@ -23,44 +26,44 @@ class Command(BaseCommand):
|
|||
script = available_scripts[name]
|
||||
except KeyError:
|
||||
raise CommandError(
|
||||
'{} is not a valid script. Run python manage.py script for a '
|
||||
'list of available scripts'.format(name))
|
||||
"{} is not a valid script. Run python manage.py script for a "
|
||||
"list of available scripts".format(name)
|
||||
)
|
||||
|
||||
self.stdout.write('')
|
||||
if options['interactive']:
|
||||
self.stdout.write("")
|
||||
if options["interactive"]:
|
||||
message = (
|
||||
'Are you sure you want to execute the script {}?\n\n'
|
||||
"Are you sure you want to execute the script {}?\n\n"
|
||||
"Type 'yes' to continue, or 'no' to cancel: "
|
||||
).format(name)
|
||||
if input(''.join(message)) != 'yes':
|
||||
if input("".join(message)) != "yes":
|
||||
raise CommandError("Script cancelled.")
|
||||
script['entrypoint'](self, **options)
|
||||
script["entrypoint"](self, **options)
|
||||
|
||||
def show_help(self):
|
||||
indentation = 4
|
||||
self.stdout.write('')
|
||||
self.stdout.write('Available scripts:')
|
||||
self.stdout.write('Launch with: python manage.py <script_name>')
|
||||
self.stdout.write("")
|
||||
self.stdout.write("Available scripts:")
|
||||
self.stdout.write("Launch with: python manage.py <script_name>")
|
||||
available_scripts = self.get_scripts()
|
||||
for name, script in sorted(available_scripts.items()):
|
||||
self.stdout.write('')
|
||||
self.stdout.write("")
|
||||
self.stdout.write(self.style.SUCCESS(name))
|
||||
self.stdout.write('')
|
||||
for line in script['help'].splitlines():
|
||||
self.stdout.write(' {}'.format(line))
|
||||
self.stdout.write('')
|
||||
self.stdout.write("")
|
||||
for line in script["help"].splitlines():
|
||||
self.stdout.write(" {}".format(line))
|
||||
self.stdout.write("")
|
||||
|
||||
def get_scripts(self):
|
||||
available_scripts = [
|
||||
k for k in sorted(scripts.__dict__.keys())
|
||||
if not k.startswith('__')
|
||||
k for k in sorted(scripts.__dict__.keys()) if not k.startswith("__")
|
||||
]
|
||||
data = {}
|
||||
for name in available_scripts:
|
||||
module = getattr(scripts, name)
|
||||
data[name] = {
|
||||
'name': name,
|
||||
'help': module.__doc__.strip(),
|
||||
'entrypoint': module.main
|
||||
"name": name,
|
||||
"help": module.__doc__.strip(),
|
||||
"entrypoint": module.main,
|
||||
}
|
||||
return data
|
||||
|
|
|
@ -7,6 +7,4 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
UnaccentExtension()
|
||||
]
|
||||
operations = [UnaccentExtension()]
|
||||
|
|
|
@ -2,5 +2,5 @@ from rest_framework.pagination import PageNumberPagination
|
|||
|
||||
|
||||
class FunkwhalePagination(PageNumberPagination):
|
||||
page_size_query_param = 'page_size'
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 50
|
||||
|
|
|
@ -9,9 +9,8 @@ from funkwhale_api.common import preferences
|
|||
|
||||
|
||||
class ConditionalAuthentication(BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if preferences.get('common__api_authentication_required'):
|
||||
if preferences.get("common__api_authentication_required"):
|
||||
return request.user and request.user.is_authenticated
|
||||
return True
|
||||
|
||||
|
@ -28,24 +27,25 @@ class OwnerPermission(BasePermission):
|
|||
owner_field = 'owner'
|
||||
owner_checks = ['read', 'write']
|
||||
"""
|
||||
|
||||
perms_map = {
|
||||
'GET': 'read',
|
||||
'OPTIONS': 'read',
|
||||
'HEAD': 'read',
|
||||
'POST': 'write',
|
||||
'PUT': 'write',
|
||||
'PATCH': 'write',
|
||||
'DELETE': 'write',
|
||||
"GET": "read",
|
||||
"OPTIONS": "read",
|
||||
"HEAD": "read",
|
||||
"POST": "write",
|
||||
"PUT": "write",
|
||||
"PATCH": "write",
|
||||
"DELETE": "write",
|
||||
}
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
method_check = self.perms_map[request.method]
|
||||
owner_checks = getattr(view, 'owner_checks', ['read', 'write'])
|
||||
owner_checks = getattr(view, "owner_checks", ["read", "write"])
|
||||
if method_check not in owner_checks:
|
||||
# check not enabled
|
||||
return True
|
||||
|
||||
owner_field = getattr(view, 'owner_field', 'user')
|
||||
owner_field = getattr(view, "owner_field", "user")
|
||||
owner = operator.attrgetter(owner_field)(obj)
|
||||
if owner != request.user:
|
||||
raise Http404
|
||||
|
|
|
@ -17,7 +17,7 @@ def get(pref):
|
|||
|
||||
|
||||
class StringListSerializer(serializers.BaseSerializer):
|
||||
separator = ','
|
||||
separator = ","
|
||||
sort = True
|
||||
|
||||
@classmethod
|
||||
|
@ -27,8 +27,8 @@ class StringListSerializer(serializers.BaseSerializer):
|
|||
|
||||
if type(value) not in [list, tuple]:
|
||||
raise cls.exception(
|
||||
"Cannot serialize, value {} is not a list or a tuple".format(
|
||||
value))
|
||||
"Cannot serialize, value {} is not a list or a tuple".format(value)
|
||||
)
|
||||
|
||||
if cls.sort:
|
||||
value = sorted(value)
|
||||
|
@ -38,7 +38,7 @@ class StringListSerializer(serializers.BaseSerializer):
|
|||
def to_python(cls, value, **kwargs):
|
||||
if not value:
|
||||
return []
|
||||
return value.split(',')
|
||||
return value.split(",")
|
||||
|
||||
|
||||
class StringListPreference(types.BasePreferenceType):
|
||||
|
@ -47,5 +47,5 @@ class StringListPreference(types.BasePreferenceType):
|
|||
|
||||
def get_api_additional_data(self):
|
||||
d = super(StringListPreference, self).get_api_additional_data()
|
||||
d['choices'] = self.get('choices')
|
||||
d["choices"] = self.get("choices")
|
||||
return d
|
||||
|
|
|
@ -8,22 +8,22 @@ from funkwhale_api.users import models
|
|||
from django.contrib.auth.models import Permission
|
||||
|
||||
mapping = {
|
||||
'dynamic_preferences.change_globalpreferencemodel': 'settings',
|
||||
'music.add_importbatch': 'library',
|
||||
'federation.change_library': 'federation',
|
||||
"dynamic_preferences.change_globalpreferencemodel": "settings",
|
||||
"music.add_importbatch": "library",
|
||||
"federation.change_library": "federation",
|
||||
}
|
||||
|
||||
|
||||
def main(command, **kwargs):
|
||||
for codename, user_permission in sorted(mapping.items()):
|
||||
app_label, c = codename.split('.')
|
||||
p = Permission.objects.get(
|
||||
content_type__app_label=app_label, codename=c)
|
||||
app_label, c = codename.split(".")
|
||||
p = Permission.objects.get(content_type__app_label=app_label, codename=c)
|
||||
users = models.User.objects.filter(
|
||||
Q(groups__permissions=p) | Q(user_permissions=p)).distinct()
|
||||
Q(groups__permissions=p) | Q(user_permissions=p)
|
||||
).distinct()
|
||||
total = users.count()
|
||||
|
||||
command.stdout.write('Updating {} users with {} permission...'.format(
|
||||
total, user_permission
|
||||
))
|
||||
users.update(**{'permission_{}'.format(user_permission): True})
|
||||
command.stdout.write(
|
||||
"Updating {} users with {} permission...".format(total, user_permission)
|
||||
)
|
||||
users.update(**{"permission_{}".format(user_permission): True})
|
||||
|
|
|
@ -5,4 +5,4 @@ You can launch it just to check how it works.
|
|||
|
||||
|
||||
def main(command, **kwargs):
|
||||
command.stdout.write('Test script run successfully')
|
||||
command.stdout.write("Test script run successfully")
|
||||
|
|
|
@ -17,67 +17,68 @@ class ActionSerializer(serializers.Serializer):
|
|||
dangerous_actions = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.queryset = kwargs.pop('queryset')
|
||||
self.queryset = kwargs.pop("queryset")
|
||||
if self.actions is None:
|
||||
raise ValueError(
|
||||
'You must declare a list of actions on '
|
||||
'the serializer class')
|
||||
"You must declare a list of actions on " "the serializer class"
|
||||
)
|
||||
|
||||
for action in self.actions:
|
||||
handler_name = 'handle_{}'.format(action)
|
||||
assert hasattr(self, handler_name), (
|
||||
'{} miss a {} method'.format(
|
||||
self.__class__.__name__, handler_name)
|
||||
handler_name = "handle_{}".format(action)
|
||||
assert hasattr(self, handler_name), "{} miss a {} method".format(
|
||||
self.__class__.__name__, handler_name
|
||||
)
|
||||
super().__init__(self, *args, **kwargs)
|
||||
|
||||
def validate_action(self, value):
|
||||
if value not in self.actions:
|
||||
raise serializers.ValidationError(
|
||||
'{} is not a valid action. Pick one of {}.'.format(
|
||||
value, ', '.join(self.actions)
|
||||
"{} is not a valid action. Pick one of {}.".format(
|
||||
value, ", ".join(self.actions)
|
||||
)
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_objects(self, value):
|
||||
qs = None
|
||||
if value == 'all':
|
||||
return self.queryset.all().order_by('id')
|
||||
if value == "all":
|
||||
return self.queryset.all().order_by("id")
|
||||
if type(value) in [list, tuple]:
|
||||
return self.queryset.filter(pk__in=value).order_by('id')
|
||||
return self.queryset.filter(pk__in=value).order_by("id")
|
||||
|
||||
raise serializers.ValidationError(
|
||||
'{} is not a valid value for objects. You must provide either a '
|
||||
'list of identifiers or the string "all".'.format(value))
|
||||
"{} is not a valid value for objects. You must provide either a "
|
||||
'list of identifiers or the string "all".'.format(value)
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
dangerous = data['action'] in self.dangerous_actions
|
||||
if dangerous and self.initial_data['objects'] == 'all':
|
||||
dangerous = data["action"] in self.dangerous_actions
|
||||
if dangerous and self.initial_data["objects"] == "all":
|
||||
raise serializers.ValidationError(
|
||||
'This action is to dangerous to be applied to all objects')
|
||||
if self.filterset_class and 'filters' in data:
|
||||
"This action is to dangerous to be applied to all objects"
|
||||
)
|
||||
if self.filterset_class and "filters" in data:
|
||||
qs_filterset = self.filterset_class(
|
||||
data['filters'], queryset=data['objects'])
|
||||
data["filters"], queryset=data["objects"]
|
||||
)
|
||||
try:
|
||||
assert qs_filterset.form.is_valid()
|
||||
except (AssertionError, TypeError):
|
||||
raise serializers.ValidationError('Invalid filters')
|
||||
data['objects'] = qs_filterset.qs
|
||||
raise serializers.ValidationError("Invalid filters")
|
||||
data["objects"] = qs_filterset.qs
|
||||
|
||||
data['count'] = data['objects'].count()
|
||||
if data['count'] < 1:
|
||||
raise serializers.ValidationError(
|
||||
'No object matching your request')
|
||||
data["count"] = data["objects"].count()
|
||||
if data["count"] < 1:
|
||||
raise serializers.ValidationError("No object matching your request")
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
handler_name = 'handle_{}'.format(self.validated_data['action'])
|
||||
handler_name = "handle_{}".format(self.validated_data["action"])
|
||||
handler = getattr(self, handler_name)
|
||||
result = handler(self.validated_data['objects'])
|
||||
result = handler(self.validated_data["objects"])
|
||||
payload = {
|
||||
'updated': self.validated_data['count'],
|
||||
'action': self.validated_data['action'],
|
||||
'result': result,
|
||||
"updated": self.validated_data["count"],
|
||||
"action": self.validated_data["action"],
|
||||
"result": result,
|
||||
}
|
||||
return payload
|
||||
|
|
|
@ -6,13 +6,12 @@ import funkwhale_api
|
|||
|
||||
|
||||
def get_user_agent():
|
||||
return 'python-requests (funkwhale/{}; +{})'.format(
|
||||
funkwhale_api.__version__,
|
||||
settings.FUNKWHALE_URL
|
||||
return "python-requests (funkwhale/{}; +{})".format(
|
||||
funkwhale_api.__version__, settings.FUNKWHALE_URL
|
||||
)
|
||||
|
||||
|
||||
def get_session():
|
||||
s = requests.Session()
|
||||
s.headers['User-Agent'] = get_user_agent()
|
||||
s.headers["User-Agent"] = get_user_agent()
|
||||
return s
|
||||
|
|
|
@ -7,6 +7,7 @@ class ASCIIFileSystemStorage(FileSystemStorage):
|
|||
"""
|
||||
Convert unicode characters in name to ASCII characters.
|
||||
"""
|
||||
|
||||
def get_valid_name(self, name):
|
||||
name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore')
|
||||
name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore")
|
||||
return super().get_valid_name(name)
|
||||
|
|
|
@ -9,13 +9,13 @@ def rename_file(instance, field_name, new_name, allow_missing_file=False):
|
|||
field = getattr(instance, field_name)
|
||||
current_name, extension = os.path.splitext(field.name)
|
||||
|
||||
new_name_with_extension = '{}{}'.format(new_name, extension)
|
||||
new_name_with_extension = "{}{}".format(new_name, extension)
|
||||
try:
|
||||
shutil.move(field.path, new_name_with_extension)
|
||||
except FileNotFoundError:
|
||||
if not allow_missing_file:
|
||||
raise
|
||||
print('Skipped missing file', field.path)
|
||||
print("Skipped missing file", field.path)
|
||||
initial_path = os.path.dirname(field.name)
|
||||
field.name = os.path.join(initial_path, new_name_with_extension)
|
||||
instance.save()
|
||||
|
@ -23,9 +23,7 @@ def rename_file(instance, field_name, new_name, allow_missing_file=False):
|
|||
|
||||
|
||||
def on_commit(f, *args, **kwargs):
|
||||
return transaction.on_commit(
|
||||
lambda: f(*args, **kwargs)
|
||||
)
|
||||
return transaction.on_commit(lambda: f(*args, **kwargs))
|
||||
|
||||
|
||||
def set_query_parameter(url, **kwargs):
|
||||
|
|
|
@ -7,25 +7,39 @@ import django.contrib.sites.models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Site',
|
||||
name="Site",
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
|
||||
('domain', models.CharField(verbose_name='domain name', max_length=100, validators=[django.contrib.sites.models._simple_domain_name_validator])),
|
||||
('name', models.CharField(verbose_name='display name', max_length=50)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"domain",
|
||||
models.CharField(
|
||||
verbose_name="domain name",
|
||||
max_length=100,
|
||||
validators=[
|
||||
django.contrib.sites.models._simple_domain_name_validator
|
||||
],
|
||||
),
|
||||
),
|
||||
("name", models.CharField(verbose_name="display name", max_length=50)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'sites',
|
||||
'verbose_name': 'site',
|
||||
'db_table': 'django_site',
|
||||
'ordering': ('domain',),
|
||||
"verbose_name_plural": "sites",
|
||||
"verbose_name": "site",
|
||||
"db_table": "django_site",
|
||||
"ordering": ("domain",),
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.sites.models.SiteManager()),
|
||||
],
|
||||
),
|
||||
managers=[("objects", django.contrib.sites.models.SiteManager())],
|
||||
)
|
||||
]
|
||||
|
|
|
@ -10,10 +10,7 @@ def update_site_forward(apps, schema_editor):
|
|||
Site = apps.get_model("sites", "Site")
|
||||
Site.objects.update_or_create(
|
||||
id=settings.SITE_ID,
|
||||
defaults={
|
||||
"domain": "funkwhale.io",
|
||||
"name": "funkwhale_api"
|
||||
}
|
||||
defaults={"domain": "funkwhale.io", "name": "funkwhale_api"},
|
||||
)
|
||||
|
||||
|
||||
|
@ -21,20 +18,12 @@ def update_site_backward(apps, schema_editor):
|
|||
"""Revert site domain and name to default."""
|
||||
Site = apps.get_model("sites", "Site")
|
||||
Site.objects.update_or_create(
|
||||
id=settings.SITE_ID,
|
||||
defaults={
|
||||
"domain": "example.com",
|
||||
"name": "example.com"
|
||||
}
|
||||
id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"}
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sites', '0001_initial'),
|
||||
]
|
||||
dependencies = [("sites", "0001_initial")]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_site_forward, update_site_backward),
|
||||
]
|
||||
operations = [migrations.RunPython(update_site_forward, update_site_backward)]
|
||||
|
|
|
@ -8,20 +8,21 @@ from django.db import migrations, models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sites', '0002_set_site_domain_and_name'),
|
||||
]
|
||||
dependencies = [("sites", "0002_set_site_domain_and_name")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name='site',
|
||||
managers=[
|
||||
('objects', django.contrib.sites.models.SiteManager()),
|
||||
],
|
||||
name="site",
|
||||
managers=[("objects", django.contrib.sites.models.SiteManager())],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='site',
|
||||
name='domain',
|
||||
field=models.CharField(max_length=100, unique=True, validators=[django.contrib.sites.models._simple_domain_name_validator], verbose_name='domain name'),
|
||||
model_name="site",
|
||||
name="domain",
|
||||
field=models.CharField(
|
||||
max_length=100,
|
||||
unique=True,
|
||||
validators=[django.contrib.sites.models._simple_domain_name_validator],
|
||||
verbose_name="domain name",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -7,20 +7,15 @@ import glob
|
|||
|
||||
|
||||
def download(
|
||||
url,
|
||||
target_directory=settings.MEDIA_ROOT,
|
||||
name="%(id)s.%(ext)s",
|
||||
bitrate=192):
|
||||
url, target_directory=settings.MEDIA_ROOT, name="%(id)s.%(ext)s", bitrate=192
|
||||
):
|
||||
target_path = os.path.join(target_directory, name)
|
||||
ydl_opts = {
|
||||
'quiet': True,
|
||||
'outtmpl': target_path,
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'vorbis',
|
||||
}],
|
||||
"quiet": True,
|
||||
"outtmpl": target_path,
|
||||
"postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "vorbis"}],
|
||||
}
|
||||
_downloader = youtube_dl.YoutubeDL(ydl_opts)
|
||||
info = _downloader.extract_info(url)
|
||||
info['audio_file_path'] = target_path % {'id': info['id'], 'ext': 'ogg'}
|
||||
info["audio_file_path"] = target_path % {"id": info["id"], "ext": "ogg"}
|
||||
return info
|
||||
|
|
|
@ -3,7 +3,7 @@ import persisting_theory
|
|||
|
||||
|
||||
class FactoriesRegistry(persisting_theory.Registry):
|
||||
look_into = 'factories'
|
||||
look_into = "factories"
|
||||
|
||||
def prepare_name(self, data, name=None):
|
||||
return name or data._meta.model._meta.label
|
||||
|
|
|
@ -3,17 +3,14 @@ from funkwhale_api.activity import record
|
|||
|
||||
from . import serializers
|
||||
|
||||
record.registry.register_serializer(
|
||||
serializers.TrackFavoriteActivitySerializer)
|
||||
record.registry.register_serializer(serializers.TrackFavoriteActivitySerializer)
|
||||
|
||||
|
||||
@record.registry.register_consumer('favorites.TrackFavorite')
|
||||
@record.registry.register_consumer("favorites.TrackFavorite")
|
||||
def broadcast_track_favorite_to_instance_activity(data, obj):
|
||||
if obj.user.privacy_level not in ['instance', 'everyone']:
|
||||
if obj.user.privacy_level not in ["instance", "everyone"]:
|
||||
return
|
||||
|
||||
channels.group_send('instance_activity', {
|
||||
'type': 'event.send',
|
||||
'text': '',
|
||||
'data': data
|
||||
})
|
||||
channels.group_send(
|
||||
"instance_activity", {"type": "event.send", "text": "", "data": data}
|
||||
)
|
||||
|
|
|
@ -5,8 +5,5 @@ from . import models
|
|||
|
||||
@admin.register(models.TrackFavorite)
|
||||
class TrackFavoriteAdmin(admin.ModelAdmin):
|
||||
list_display = ['user', 'track', 'creation_date']
|
||||
list_select_related = [
|
||||
'user',
|
||||
'track'
|
||||
]
|
||||
list_display = ["user", "track", "creation_date"]
|
||||
list_select_related = ["user", "track"]
|
||||
|
|
|
@ -12,4 +12,4 @@ class TrackFavorite(factory.django.DjangoModelFactory):
|
|||
user = factory.SubFactory(UserFactory)
|
||||
|
||||
class Meta:
|
||||
model = 'favorites.TrackFavorite'
|
||||
model = "favorites.TrackFavorite"
|
||||
|
|
|
@ -9,25 +9,47 @@ from django.conf import settings
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0003_auto_20151222_2233'),
|
||||
("music", "0003_auto_20151222_2233"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TrackFavorite',
|
||||
name="TrackFavorite",
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('track', models.ForeignKey(related_name='track_favorites', to='music.Track', on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(related_name='track_favorites', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
verbose_name="ID",
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
(
|
||||
"track",
|
||||
models.ForeignKey(
|
||||
related_name="track_favorites",
|
||||
to="music.Track",
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
related_name="track_favorites",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-creation_date',),
|
||||
},
|
||||
options={"ordering": ("-creation_date",)},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='trackfavorite',
|
||||
unique_together=set([('track', 'user')]),
|
||||
name="trackfavorite", unique_together=set([("track", "user")])
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,13 +8,15 @@ from funkwhale_api.music.models import Track
|
|||
class TrackFavorite(models.Model):
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
user = models.ForeignKey(
|
||||
'users.User', related_name='track_favorites', on_delete=models.CASCADE)
|
||||
"users.User", related_name="track_favorites", on_delete=models.CASCADE
|
||||
)
|
||||
track = models.ForeignKey(
|
||||
Track, related_name='track_favorites', on_delete=models.CASCADE)
|
||||
Track, related_name="track_favorites", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('track', 'user')
|
||||
ordering = ('-creation_date',)
|
||||
unique_together = ("track", "user")
|
||||
ordering = ("-creation_date",)
|
||||
|
||||
@classmethod
|
||||
def add(cls, track, user):
|
||||
|
@ -22,5 +24,4 @@ class TrackFavorite(models.Model):
|
|||
return favorite
|
||||
|
||||
def get_activity_url(self):
|
||||
return '{}/favorites/tracks/{}'.format(
|
||||
self.user.get_activity_url(), self.pk)
|
||||
return "{}/favorites/tracks/{}".format(self.user.get_activity_url(), self.pk)
|
||||
|
|
|
@ -11,29 +11,22 @@ from . import models
|
|||
|
||||
class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer):
|
||||
type = serializers.SerializerMethodField()
|
||||
object = TrackActivitySerializer(source='track')
|
||||
actor = UserActivitySerializer(source='user')
|
||||
published = serializers.DateTimeField(source='creation_date')
|
||||
object = TrackActivitySerializer(source="track")
|
||||
actor = UserActivitySerializer(source="user")
|
||||
published = serializers.DateTimeField(source="creation_date")
|
||||
|
||||
class Meta:
|
||||
model = models.TrackFavorite
|
||||
fields = [
|
||||
'id',
|
||||
'local_id',
|
||||
'object',
|
||||
'type',
|
||||
'actor',
|
||||
'published'
|
||||
]
|
||||
fields = ["id", "local_id", "object", "type", "actor", "published"]
|
||||
|
||||
def get_actor(self, obj):
|
||||
return UserActivitySerializer(obj.user).data
|
||||
|
||||
def get_type(self, obj):
|
||||
return 'Like'
|
||||
return "Like"
|
||||
|
||||
|
||||
class UserTrackFavoriteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.TrackFavorite
|
||||
fields = ('id', 'track', 'creation_date')
|
||||
fields = ("id", "track", "creation_date")
|
||||
|
|
|
@ -2,7 +2,8 @@ from django.conf.urls import include, url
|
|||
from . import views
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r'tracks', views.TrackFavoriteViewSet, 'tracks')
|
||||
router.register(r"tracks", views.TrackFavoriteViewSet, "tracks")
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
|
|
@ -12,13 +12,15 @@ from . import models
|
|||
from . import serializers
|
||||
|
||||
|
||||
class TrackFavoriteViewSet(mixins.CreateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
class TrackFavoriteViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
|
||||
serializer_class = serializers.UserTrackFavoriteSerializer
|
||||
queryset = (models.TrackFavorite.objects.all())
|
||||
queryset = models.TrackFavorite.objects.all()
|
||||
permission_classes = [ConditionalAuthentication]
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
|
@ -28,20 +30,22 @@ class TrackFavoriteViewSet(mixins.CreateModelMixin,
|
|||
serializer = self.get_serializer(instance=instance)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
record.send(instance)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(user=self.request.user)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
track = Track.objects.get(pk=serializer.data['track'])
|
||||
track = Track.objects.get(pk=serializer.data["track"])
|
||||
favorite = models.TrackFavorite.add(track=track, user=self.request.user)
|
||||
return favorite
|
||||
|
||||
@list_route(methods=['delete', 'post'])
|
||||
@list_route(methods=["delete", "post"])
|
||||
def remove(self, request, *args, **kwargs):
|
||||
try:
|
||||
pk = int(request.data['track'])
|
||||
pk = int(request.data["track"])
|
||||
favorite = request.user.track_favorites.get(track__pk=pk)
|
||||
except (AttributeError, ValueError, models.TrackFavorite.DoesNotExist):
|
||||
return Response({}, status=400)
|
||||
|
|
|
@ -2,66 +2,59 @@ from . import serializers
|
|||
from . import tasks
|
||||
|
||||
ACTIVITY_TYPES = [
|
||||
'Accept',
|
||||
'Add',
|
||||
'Announce',
|
||||
'Arrive',
|
||||
'Block',
|
||||
'Create',
|
||||
'Delete',
|
||||
'Dislike',
|
||||
'Flag',
|
||||
'Follow',
|
||||
'Ignore',
|
||||
'Invite',
|
||||
'Join',
|
||||
'Leave',
|
||||
'Like',
|
||||
'Listen',
|
||||
'Move',
|
||||
'Offer',
|
||||
'Question',
|
||||
'Reject',
|
||||
'Read',
|
||||
'Remove',
|
||||
'TentativeReject',
|
||||
'TentativeAccept',
|
||||
'Travel',
|
||||
'Undo',
|
||||
'Update',
|
||||
'View',
|
||||
"Accept",
|
||||
"Add",
|
||||
"Announce",
|
||||
"Arrive",
|
||||
"Block",
|
||||
"Create",
|
||||
"Delete",
|
||||
"Dislike",
|
||||
"Flag",
|
||||
"Follow",
|
||||
"Ignore",
|
||||
"Invite",
|
||||
"Join",
|
||||
"Leave",
|
||||
"Like",
|
||||
"Listen",
|
||||
"Move",
|
||||
"Offer",
|
||||
"Question",
|
||||
"Reject",
|
||||
"Read",
|
||||
"Remove",
|
||||
"TentativeReject",
|
||||
"TentativeAccept",
|
||||
"Travel",
|
||||
"Undo",
|
||||
"Update",
|
||||
"View",
|
||||
]
|
||||
|
||||
|
||||
OBJECT_TYPES = [
|
||||
'Article',
|
||||
'Audio',
|
||||
'Collection',
|
||||
'Document',
|
||||
'Event',
|
||||
'Image',
|
||||
'Note',
|
||||
'OrderedCollection',
|
||||
'Page',
|
||||
'Place',
|
||||
'Profile',
|
||||
'Relationship',
|
||||
'Tombstone',
|
||||
'Video',
|
||||
"Article",
|
||||
"Audio",
|
||||
"Collection",
|
||||
"Document",
|
||||
"Event",
|
||||
"Image",
|
||||
"Note",
|
||||
"OrderedCollection",
|
||||
"Page",
|
||||
"Place",
|
||||
"Profile",
|
||||
"Relationship",
|
||||
"Tombstone",
|
||||
"Video",
|
||||
] + ACTIVITY_TYPES
|
||||
|
||||
|
||||
def deliver(activity, on_behalf_of, to=[]):
|
||||
return tasks.send.delay(
|
||||
activity=activity,
|
||||
actor_id=on_behalf_of.pk,
|
||||
to=to
|
||||
)
|
||||
return tasks.send.delay(activity=activity, actor_id=on_behalf_of.pk, to=to)
|
||||
|
||||
|
||||
def accept_follow(follow):
|
||||
serializer = serializers.AcceptFollowSerializer(follow)
|
||||
return deliver(
|
||||
serializer.data,
|
||||
to=[follow.actor.url],
|
||||
on_behalf_of=follow.target)
|
||||
return deliver(serializer.data, to=[follow.actor.url], on_behalf_of=follow.target)
|
||||
|
|
|
@ -29,8 +29,10 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def remove_tags(text):
|
||||
logger.debug('Removing tags from %s', text)
|
||||
return ''.join(xml.etree.ElementTree.fromstring('<div>{}</div>'.format(text)).itertext())
|
||||
logger.debug("Removing tags from %s", text)
|
||||
return "".join(
|
||||
xml.etree.ElementTree.fromstring("<div>{}</div>".format(text)).itertext()
|
||||
)
|
||||
|
||||
|
||||
def get_actor_data(actor_url):
|
||||
|
@ -38,16 +40,13 @@ def get_actor_data(actor_url):
|
|||
actor_url,
|
||||
timeout=5,
|
||||
verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
|
||||
headers={
|
||||
'Accept': 'application/activity+json',
|
||||
}
|
||||
headers={"Accept": "application/activity+json"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
try:
|
||||
return response.json()
|
||||
except:
|
||||
raise ValueError(
|
||||
'Invalid actor payload: {}'.format(response.text))
|
||||
raise ValueError("Invalid actor payload: {}".format(response.text))
|
||||
|
||||
|
||||
def get_actor(actor_url):
|
||||
|
@ -56,7 +55,8 @@ def get_actor(actor_url):
|
|||
except models.Actor.DoesNotExist:
|
||||
actor = None
|
||||
fetch_delta = datetime.timedelta(
|
||||
minutes=preferences.get('federation__actor_fetch_delay'))
|
||||
minutes=preferences.get("federation__actor_fetch_delay")
|
||||
)
|
||||
if actor and actor.last_fetch_date > timezone.now() - fetch_delta:
|
||||
# cache is hot, we can return as is
|
||||
return actor
|
||||
|
@ -73,8 +73,7 @@ class SystemActor(object):
|
|||
|
||||
def get_request_auth(self):
|
||||
actor = self.get_actor_instance()
|
||||
return signing.get_auth(
|
||||
actor.private_key, actor.private_key_id)
|
||||
return signing.get_auth(actor.private_key, actor.private_key_id)
|
||||
|
||||
def serialize(self):
|
||||
actor = self.get_actor_instance()
|
||||
|
@ -88,42 +87,35 @@ class SystemActor(object):
|
|||
pass
|
||||
private, public = keys.get_key_pair()
|
||||
args = self.get_instance_argument(
|
||||
self.id,
|
||||
name=self.name,
|
||||
summary=self.summary,
|
||||
**self.additional_attributes
|
||||
self.id, name=self.name, summary=self.summary, **self.additional_attributes
|
||||
)
|
||||
args['private_key'] = private.decode('utf-8')
|
||||
args['public_key'] = public.decode('utf-8')
|
||||
args["private_key"] = private.decode("utf-8")
|
||||
args["public_key"] = public.decode("utf-8")
|
||||
return models.Actor.objects.create(**args)
|
||||
|
||||
def get_actor_url(self):
|
||||
return utils.full_url(
|
||||
reverse(
|
||||
'federation:instance-actors-detail',
|
||||
kwargs={'actor': self.id}))
|
||||
reverse("federation:instance-actors-detail", kwargs={"actor": self.id})
|
||||
)
|
||||
|
||||
def get_instance_argument(self, id, name, summary, **kwargs):
|
||||
p = {
|
||||
'preferred_username': id,
|
||||
'domain': settings.FEDERATION_HOSTNAME,
|
||||
'type': 'Person',
|
||||
'name': name.format(host=settings.FEDERATION_HOSTNAME),
|
||||
'manually_approves_followers': True,
|
||||
'url': self.get_actor_url(),
|
||||
'shared_inbox_url': utils.full_url(
|
||||
reverse(
|
||||
'federation:instance-actors-inbox',
|
||||
kwargs={'actor': id})),
|
||||
'inbox_url': utils.full_url(
|
||||
reverse(
|
||||
'federation:instance-actors-inbox',
|
||||
kwargs={'actor': id})),
|
||||
'outbox_url': utils.full_url(
|
||||
reverse(
|
||||
'federation:instance-actors-outbox',
|
||||
kwargs={'actor': id})),
|
||||
'summary': summary.format(host=settings.FEDERATION_HOSTNAME)
|
||||
"preferred_username": id,
|
||||
"domain": settings.FEDERATION_HOSTNAME,
|
||||
"type": "Person",
|
||||
"name": name.format(host=settings.FEDERATION_HOSTNAME),
|
||||
"manually_approves_followers": True,
|
||||
"url": self.get_actor_url(),
|
||||
"shared_inbox_url": utils.full_url(
|
||||
reverse("federation:instance-actors-inbox", kwargs={"actor": id})
|
||||
),
|
||||
"inbox_url": utils.full_url(
|
||||
reverse("federation:instance-actors-inbox", kwargs={"actor": id})
|
||||
),
|
||||
"outbox_url": utils.full_url(
|
||||
reverse("federation:instance-actors-outbox", kwargs={"actor": id})
|
||||
),
|
||||
"summary": summary.format(host=settings.FEDERATION_HOSTNAME),
|
||||
}
|
||||
p.update(kwargs)
|
||||
return p
|
||||
|
@ -145,22 +137,19 @@ class SystemActor(object):
|
|||
Main entrypoint for handling activities posted to the
|
||||
actor's inbox
|
||||
"""
|
||||
logger.info('Received activity on %s inbox', self.id)
|
||||
logger.info("Received activity on %s inbox", self.id)
|
||||
|
||||
if actor is None:
|
||||
raise PermissionDenied('Actor not authenticated')
|
||||
raise PermissionDenied("Actor not authenticated")
|
||||
|
||||
serializer = serializers.ActivitySerializer(
|
||||
data=data, context={'actor': actor})
|
||||
serializer = serializers.ActivitySerializer(data=data, context={"actor": actor})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
ac = serializer.data
|
||||
try:
|
||||
handler = getattr(
|
||||
self, 'handle_{}'.format(ac['type'].lower()))
|
||||
handler = getattr(self, "handle_{}".format(ac["type"].lower()))
|
||||
except (KeyError, AttributeError):
|
||||
logger.debug(
|
||||
'No handler for activity %s', ac['type'])
|
||||
logger.debug("No handler for activity %s", ac["type"])
|
||||
return
|
||||
|
||||
return handler(data, actor)
|
||||
|
@ -168,9 +157,10 @@ class SystemActor(object):
|
|||
def handle_follow(self, ac, sender):
|
||||
system_actor = self.get_actor_instance()
|
||||
serializer = serializers.FollowSerializer(
|
||||
data=ac, context={'follow_actor': sender})
|
||||
data=ac, context={"follow_actor": sender}
|
||||
)
|
||||
if not serializer.is_valid():
|
||||
return logger.info('Invalid follow payload')
|
||||
return logger.info("Invalid follow payload")
|
||||
approved = True if not self.manually_approves_followers else None
|
||||
follow = serializer.save(approved=approved)
|
||||
if follow.approved:
|
||||
|
@ -179,26 +169,27 @@ class SystemActor(object):
|
|||
def handle_accept(self, ac, sender):
|
||||
system_actor = self.get_actor_instance()
|
||||
serializer = serializers.AcceptFollowSerializer(
|
||||
data=ac,
|
||||
context={'follow_target': sender, 'follow_actor': system_actor})
|
||||
data=ac, context={"follow_target": sender, "follow_actor": system_actor}
|
||||
)
|
||||
if not serializer.is_valid(raise_exception=True):
|
||||
return logger.info('Received invalid payload')
|
||||
return logger.info("Received invalid payload")
|
||||
|
||||
return serializer.save()
|
||||
|
||||
def handle_undo_follow(self, ac, sender):
|
||||
system_actor = self.get_actor_instance()
|
||||
serializer = serializers.UndoFollowSerializer(
|
||||
data=ac, context={'actor': sender, 'target': system_actor})
|
||||
data=ac, context={"actor": sender, "target": system_actor}
|
||||
)
|
||||
if not serializer.is_valid():
|
||||
return logger.info('Received invalid payload')
|
||||
return logger.info("Received invalid payload")
|
||||
serializer.save()
|
||||
|
||||
def handle_undo(self, ac, sender):
|
||||
if ac['object']['type'] != 'Follow':
|
||||
if ac["object"]["type"] != "Follow":
|
||||
return
|
||||
|
||||
if ac['object']['actor'] != sender.url:
|
||||
if ac["object"]["actor"] != sender.url:
|
||||
# not the same actor, permission issue
|
||||
return
|
||||
|
||||
|
@ -206,55 +197,52 @@ class SystemActor(object):
|
|||
|
||||
|
||||
class LibraryActor(SystemActor):
|
||||
id = 'library'
|
||||
name = '{host}\'s library'
|
||||
summary = 'Bot account to federate with {host}\'s library'
|
||||
additional_attributes = {
|
||||
'manually_approves_followers': True
|
||||
}
|
||||
id = "library"
|
||||
name = "{host}'s library"
|
||||
summary = "Bot account to federate with {host}'s library"
|
||||
additional_attributes = {"manually_approves_followers": True}
|
||||
|
||||
def serialize(self):
|
||||
data = super().serialize()
|
||||
urls = data.setdefault('url', [])
|
||||
urls.append({
|
||||
'type': 'Link',
|
||||
'mediaType': 'application/activity+json',
|
||||
'name': 'library',
|
||||
'href': utils.full_url(reverse('federation:music:files-list'))
|
||||
})
|
||||
urls = data.setdefault("url", [])
|
||||
urls.append(
|
||||
{
|
||||
"type": "Link",
|
||||
"mediaType": "application/activity+json",
|
||||
"name": "library",
|
||||
"href": utils.full_url(reverse("federation:music:files-list")),
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
@property
|
||||
def manually_approves_followers(self):
|
||||
return preferences.get('federation__music_needs_approval')
|
||||
return preferences.get("federation__music_needs_approval")
|
||||
|
||||
@transaction.atomic
|
||||
def handle_create(self, ac, sender):
|
||||
try:
|
||||
remote_library = models.Library.objects.get(
|
||||
actor=sender,
|
||||
federation_enabled=True,
|
||||
actor=sender, federation_enabled=True
|
||||
)
|
||||
except models.Library.DoesNotExist:
|
||||
logger.info(
|
||||
'Skipping import, we\'re not following %s', sender.url)
|
||||
logger.info("Skipping import, we're not following %s", sender.url)
|
||||
return
|
||||
|
||||
if ac['object']['type'] != 'Collection':
|
||||
if ac["object"]["type"] != "Collection":
|
||||
return
|
||||
|
||||
if ac['object']['totalItems'] <= 0:
|
||||
if ac["object"]["totalItems"] <= 0:
|
||||
return
|
||||
|
||||
try:
|
||||
items = ac['object']['items']
|
||||
items = ac["object"]["items"]
|
||||
except KeyError:
|
||||
logger.warning('No items in collection!')
|
||||
logger.warning("No items in collection!")
|
||||
return
|
||||
|
||||
item_serializers = [
|
||||
serializers.AudioSerializer(
|
||||
data=i, context={'library': remote_library})
|
||||
serializers.AudioSerializer(data=i, context={"library": remote_library})
|
||||
for i in items
|
||||
]
|
||||
now = timezone.now()
|
||||
|
@ -263,27 +251,21 @@ class LibraryActor(SystemActor):
|
|||
if s.is_valid():
|
||||
valid_serializers.append(s)
|
||||
else:
|
||||
logger.debug(
|
||||
'Skipping invalid item %s, %s', s.initial_data, s.errors)
|
||||
logger.debug("Skipping invalid item %s, %s", s.initial_data, s.errors)
|
||||
|
||||
lts = []
|
||||
for s in valid_serializers:
|
||||
lts.append(s.save())
|
||||
|
||||
if remote_library.autoimport:
|
||||
batch = music_models.ImportBatch.objects.create(
|
||||
source='federation',
|
||||
)
|
||||
batch = music_models.ImportBatch.objects.create(source="federation")
|
||||
for lt in lts:
|
||||
if lt.creation_date < now:
|
||||
# track was already in the library, we do not trigger
|
||||
# an import
|
||||
continue
|
||||
job = music_models.ImportJob.objects.create(
|
||||
batch=batch,
|
||||
library_track=lt,
|
||||
mbid=lt.mbid,
|
||||
source=lt.url,
|
||||
batch=batch, library_track=lt, mbid=lt.mbid, source=lt.url
|
||||
)
|
||||
funkwhale_utils.on_commit(
|
||||
music_tasks.import_job_run.delay,
|
||||
|
@ -293,15 +275,13 @@ class LibraryActor(SystemActor):
|
|||
|
||||
|
||||
class TestActor(SystemActor):
|
||||
id = 'test'
|
||||
name = '{host}\'s test account'
|
||||
id = "test"
|
||||
name = "{host}'s test account"
|
||||
summary = (
|
||||
'Bot account to test federation with {host}. '
|
||||
'Send me /ping and I\'ll answer you.'
|
||||
"Bot account to test federation with {host}. "
|
||||
"Send me /ping and I'll answer you."
|
||||
)
|
||||
additional_attributes = {
|
||||
'manually_approves_followers': False
|
||||
}
|
||||
additional_attributes = {"manually_approves_followers": False}
|
||||
manually_approves_followers = False
|
||||
|
||||
def get_outbox(self, data, actor=None):
|
||||
|
@ -309,15 +289,14 @@ class TestActor(SystemActor):
|
|||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{}
|
||||
{},
|
||||
],
|
||||
"id": utils.full_url(
|
||||
reverse(
|
||||
'federation:instance-actors-outbox',
|
||||
kwargs={'actor': self.id})),
|
||||
reverse("federation:instance-actors-outbox", kwargs={"actor": self.id})
|
||||
),
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 0,
|
||||
"orderedItems": []
|
||||
"orderedItems": [],
|
||||
}
|
||||
|
||||
def parse_command(self, message):
|
||||
|
@ -327,99 +306,86 @@ class TestActor(SystemActor):
|
|||
"""
|
||||
raw = remove_tags(message)
|
||||
try:
|
||||
return raw.split('/')[1]
|
||||
return raw.split("/")[1]
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
def handle_create(self, ac, sender):
|
||||
if ac['object']['type'] != 'Note':
|
||||
if ac["object"]["type"] != "Note":
|
||||
return
|
||||
|
||||
# we received a toot \o/
|
||||
command = self.parse_command(ac['object']['content'])
|
||||
logger.debug('Parsed command: %s', command)
|
||||
if command != 'ping':
|
||||
command = self.parse_command(ac["object"]["content"])
|
||||
logger.debug("Parsed command: %s", command)
|
||||
if command != "ping":
|
||||
return
|
||||
|
||||
now = timezone.now()
|
||||
test_actor = self.get_actor_instance()
|
||||
reply_url = 'https://{}/activities/note/{}'.format(
|
||||
reply_url = "https://{}/activities/note/{}".format(
|
||||
settings.FEDERATION_HOSTNAME, now.timestamp()
|
||||
)
|
||||
reply_content = '{} Pong!'.format(
|
||||
sender.mention_username
|
||||
)
|
||||
reply_content = "{} Pong!".format(sender.mention_username)
|
||||
reply_activity = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{}
|
||||
{},
|
||||
],
|
||||
'type': 'Create',
|
||||
'actor': test_actor.url,
|
||||
'id': '{}/activity'.format(reply_url),
|
||||
'published': now.isoformat(),
|
||||
'to': ac['actor'],
|
||||
'cc': [],
|
||||
'object': {
|
||||
'type': 'Note',
|
||||
'content': 'Pong!',
|
||||
'summary': None,
|
||||
'published': now.isoformat(),
|
||||
'id': reply_url,
|
||||
'inReplyTo': ac['object']['id'],
|
||||
'sensitive': False,
|
||||
'url': reply_url,
|
||||
'to': [ac['actor']],
|
||||
'attributedTo': test_actor.url,
|
||||
'cc': [],
|
||||
'attachment': [],
|
||||
'tag': [{
|
||||
"type": "Mention",
|
||||
"href": ac['actor'],
|
||||
"name": sender.mention_username
|
||||
}]
|
||||
}
|
||||
"type": "Create",
|
||||
"actor": test_actor.url,
|
||||
"id": "{}/activity".format(reply_url),
|
||||
"published": now.isoformat(),
|
||||
"to": ac["actor"],
|
||||
"cc": [],
|
||||
"object": {
|
||||
"type": "Note",
|
||||
"content": "Pong!",
|
||||
"summary": None,
|
||||
"published": now.isoformat(),
|
||||
"id": reply_url,
|
||||
"inReplyTo": ac["object"]["id"],
|
||||
"sensitive": False,
|
||||
"url": reply_url,
|
||||
"to": [ac["actor"]],
|
||||
"attributedTo": test_actor.url,
|
||||
"cc": [],
|
||||
"attachment": [],
|
||||
"tag": [
|
||||
{
|
||||
"type": "Mention",
|
||||
"href": ac["actor"],
|
||||
"name": sender.mention_username,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
activity.deliver(
|
||||
reply_activity,
|
||||
to=[ac['actor']],
|
||||
on_behalf_of=test_actor)
|
||||
activity.deliver(reply_activity, to=[ac["actor"]], on_behalf_of=test_actor)
|
||||
|
||||
def handle_follow(self, ac, sender):
|
||||
super().handle_follow(ac, sender)
|
||||
# also, we follow back
|
||||
test_actor = self.get_actor_instance()
|
||||
follow_back = models.Follow.objects.get_or_create(
|
||||
actor=test_actor,
|
||||
target=sender,
|
||||
approved=None,
|
||||
actor=test_actor, target=sender, approved=None
|
||||
)[0]
|
||||
activity.deliver(
|
||||
serializers.FollowSerializer(follow_back).data,
|
||||
to=[follow_back.target.url],
|
||||
on_behalf_of=follow_back.actor)
|
||||
on_behalf_of=follow_back.actor,
|
||||
)
|
||||
|
||||
def handle_undo_follow(self, ac, sender):
|
||||
super().handle_undo_follow(ac, sender)
|
||||
actor = self.get_actor_instance()
|
||||
# we also unfollow the sender, if possible
|
||||
try:
|
||||
follow = models.Follow.objects.get(
|
||||
target=sender,
|
||||
actor=actor,
|
||||
)
|
||||
follow = models.Follow.objects.get(target=sender, actor=actor)
|
||||
except models.Follow.DoesNotExist:
|
||||
return
|
||||
undo = serializers.UndoFollowSerializer(follow).data
|
||||
follow.delete()
|
||||
activity.deliver(
|
||||
undo,
|
||||
to=[sender.url],
|
||||
on_behalf_of=actor)
|
||||
activity.deliver(undo, to=[sender.url], on_behalf_of=actor)
|
||||
|
||||
|
||||
SYSTEM_ACTORS = {
|
||||
'library': LibraryActor(),
|
||||
'test': TestActor(),
|
||||
}
|
||||
SYSTEM_ACTORS = {"library": LibraryActor(), "test": TestActor()}
|
||||
|
|
|
@ -6,61 +6,43 @@ from . import models
|
|||
@admin.register(models.Actor)
|
||||
class ActorAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'url',
|
||||
'domain',
|
||||
'preferred_username',
|
||||
'type',
|
||||
'creation_date',
|
||||
'last_fetch_date']
|
||||
search_fields = ['url', 'domain', 'preferred_username']
|
||||
list_filter = [
|
||||
'type'
|
||||
"url",
|
||||
"domain",
|
||||
"preferred_username",
|
||||
"type",
|
||||
"creation_date",
|
||||
"last_fetch_date",
|
||||
]
|
||||
search_fields = ["url", "domain", "preferred_username"]
|
||||
list_filter = ["type"]
|
||||
|
||||
|
||||
@admin.register(models.Follow)
|
||||
class FollowAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'actor',
|
||||
'target',
|
||||
'approved',
|
||||
'creation_date'
|
||||
]
|
||||
list_filter = [
|
||||
'approved'
|
||||
]
|
||||
search_fields = ['actor__url', 'target__url']
|
||||
list_display = ["actor", "target", "approved", "creation_date"]
|
||||
list_filter = ["approved"]
|
||||
search_fields = ["actor__url", "target__url"]
|
||||
list_select_related = True
|
||||
|
||||
|
||||
@admin.register(models.Library)
|
||||
class LibraryAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'actor',
|
||||
'url',
|
||||
'creation_date',
|
||||
'fetched_date',
|
||||
'tracks_count']
|
||||
search_fields = ['actor__url', 'url']
|
||||
list_filter = [
|
||||
'federation_enabled',
|
||||
'download_files',
|
||||
'autoimport',
|
||||
]
|
||||
list_display = ["actor", "url", "creation_date", "fetched_date", "tracks_count"]
|
||||
search_fields = ["actor__url", "url"]
|
||||
list_filter = ["federation_enabled", "download_files", "autoimport"]
|
||||
list_select_related = True
|
||||
|
||||
|
||||
@admin.register(models.LibraryTrack)
|
||||
class LibraryTrackAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'title',
|
||||
'artist_name',
|
||||
'album_title',
|
||||
'url',
|
||||
'library',
|
||||
'creation_date',
|
||||
'published_date',
|
||||
"title",
|
||||
"artist_name",
|
||||
"album_title",
|
||||
"url",
|
||||
"library",
|
||||
"creation_date",
|
||||
"published_date",
|
||||
]
|
||||
search_fields = [
|
||||
'library__url', 'url', 'artist_name', 'title', 'album_title']
|
||||
search_fields = ["library__url", "url", "artist_name", "title", "album_title"]
|
||||
list_select_related = True
|
||||
|
|
|
@ -3,13 +3,7 @@ from rest_framework import routers
|
|||
from . import views
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(
|
||||
r'libraries',
|
||||
views.LibraryViewSet,
|
||||
'libraries')
|
||||
router.register(
|
||||
r'library-tracks',
|
||||
views.LibraryTrackViewSet,
|
||||
'library-tracks')
|
||||
router.register(r"libraries", views.LibraryViewSet, "libraries")
|
||||
router.register(r"library-tracks", views.LibraryTrackViewSet, "library-tracks")
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
|
|
@ -17,7 +17,7 @@ class SignatureAuthentication(authentication.BaseAuthentication):
|
|||
def authenticate_actor(self, request):
|
||||
headers = utils.clean_wsgi_headers(request.META)
|
||||
try:
|
||||
signature = headers['Signature']
|
||||
signature = headers["Signature"]
|
||||
key_id = keys.get_key_id_from_signature_header(signature)
|
||||
except KeyError:
|
||||
return
|
||||
|
@ -25,25 +25,25 @@ class SignatureAuthentication(authentication.BaseAuthentication):
|
|||
raise exceptions.AuthenticationFailed(str(e))
|
||||
|
||||
try:
|
||||
actor = actors.get_actor(key_id.split('#')[0])
|
||||
actor = actors.get_actor(key_id.split("#")[0])
|
||||
except Exception as e:
|
||||
raise exceptions.AuthenticationFailed(str(e))
|
||||
|
||||
if not actor.public_key:
|
||||
raise exceptions.AuthenticationFailed('No public key found')
|
||||
raise exceptions.AuthenticationFailed("No public key found")
|
||||
|
||||
try:
|
||||
signing.verify_django(request, actor.public_key.encode('utf-8'))
|
||||
signing.verify_django(request, actor.public_key.encode("utf-8"))
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
raise exceptions.AuthenticationFailed('Invalid signature')
|
||||
raise exceptions.AuthenticationFailed("Invalid signature")
|
||||
|
||||
return actor
|
||||
|
||||
def authenticate(self, request):
|
||||
setattr(request, 'actor', None)
|
||||
setattr(request, "actor", None)
|
||||
actor = self.authenticate_actor(request)
|
||||
if not actor:
|
||||
return
|
||||
user = AnonymousUser()
|
||||
setattr(request, 'actor', actor)
|
||||
setattr(request, "actor", actor)
|
||||
return (user, None)
|
||||
|
|
|
@ -4,77 +4,66 @@ from dynamic_preferences import types
|
|||
from dynamic_preferences.registries import global_preferences_registry
|
||||
|
||||
from funkwhale_api.common import preferences
|
||||
federation = types.Section('federation')
|
||||
|
||||
federation = types.Section("federation")
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class MusicCacheDuration(types.IntPreference):
|
||||
show_in_api = True
|
||||
section = federation
|
||||
name = 'music_cache_duration'
|
||||
name = "music_cache_duration"
|
||||
default = 60 * 24 * 2
|
||||
verbose_name = 'Music cache duration'
|
||||
verbose_name = "Music cache duration"
|
||||
help_text = (
|
||||
'How much minutes do you want to keep a copy of federated tracks'
|
||||
'locally? Federated files that were not listened in this interval '
|
||||
'will be erased and refetched from the remote on the next listening.'
|
||||
"How much minutes do you want to keep a copy of federated tracks"
|
||||
"locally? Federated files that were not listened in this interval "
|
||||
"will be erased and refetched from the remote on the next listening."
|
||||
)
|
||||
field_kwargs = {
|
||||
'required': False,
|
||||
}
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class Enabled(preferences.DefaultFromSettingMixin, types.BooleanPreference):
|
||||
section = federation
|
||||
name = 'enabled'
|
||||
setting = 'FEDERATION_ENABLED'
|
||||
verbose_name = 'Federation enabled'
|
||||
name = "enabled"
|
||||
setting = "FEDERATION_ENABLED"
|
||||
verbose_name = "Federation enabled"
|
||||
help_text = (
|
||||
'Use this setting to enable or disable federation logic and API'
|
||||
' globally.'
|
||||
"Use this setting to enable or disable federation logic and API" " globally."
|
||||
)
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class CollectionPageSize(
|
||||
preferences.DefaultFromSettingMixin, types.IntPreference):
|
||||
class CollectionPageSize(preferences.DefaultFromSettingMixin, types.IntPreference):
|
||||
section = federation
|
||||
name = 'collection_page_size'
|
||||
setting = 'FEDERATION_COLLECTION_PAGE_SIZE'
|
||||
verbose_name = 'Federation collection page size'
|
||||
help_text = (
|
||||
'How much items to display in ActivityPub collections.'
|
||||
)
|
||||
field_kwargs = {
|
||||
'required': False,
|
||||
}
|
||||
name = "collection_page_size"
|
||||
setting = "FEDERATION_COLLECTION_PAGE_SIZE"
|
||||
verbose_name = "Federation collection page size"
|
||||
help_text = "How much items to display in ActivityPub collections."
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class ActorFetchDelay(
|
||||
preferences.DefaultFromSettingMixin, types.IntPreference):
|
||||
class ActorFetchDelay(preferences.DefaultFromSettingMixin, types.IntPreference):
|
||||
section = federation
|
||||
name = 'actor_fetch_delay'
|
||||
setting = 'FEDERATION_ACTOR_FETCH_DELAY'
|
||||
verbose_name = 'Federation actor fetch delay'
|
||||
name = "actor_fetch_delay"
|
||||
setting = "FEDERATION_ACTOR_FETCH_DELAY"
|
||||
verbose_name = "Federation actor fetch delay"
|
||||
help_text = (
|
||||
'How much minutes to wait before refetching actors on '
|
||||
'request authentication.'
|
||||
"How much minutes to wait before refetching actors on "
|
||||
"request authentication."
|
||||
)
|
||||
field_kwargs = {
|
||||
'required': False,
|
||||
}
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class MusicNeedsApproval(
|
||||
preferences.DefaultFromSettingMixin, types.BooleanPreference):
|
||||
class MusicNeedsApproval(preferences.DefaultFromSettingMixin, types.BooleanPreference):
|
||||
section = federation
|
||||
name = 'music_needs_approval'
|
||||
setting = 'FEDERATION_MUSIC_NEEDS_APPROVAL'
|
||||
verbose_name = 'Federation music needs approval'
|
||||
name = "music_needs_approval"
|
||||
setting = "FEDERATION_MUSIC_NEEDS_APPROVAL"
|
||||
verbose_name = "Federation music needs approval"
|
||||
help_text = (
|
||||
'When true, other federation actors will need your approval'
|
||||
' before being able to browse your library.'
|
||||
"When true, other federation actors will need your approval"
|
||||
" before being able to browse your library."
|
||||
)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
class MalformedPayload(ValueError):
|
||||
pass
|
||||
|
||||
|
|
|
@ -12,29 +12,25 @@ from . import keys
|
|||
from . import models
|
||||
|
||||
|
||||
registry.register(keys.get_key_pair, name='federation.KeyPair')
|
||||
registry.register(keys.get_key_pair, name="federation.KeyPair")
|
||||
|
||||
|
||||
@registry.register(name='federation.SignatureAuth')
|
||||
@registry.register(name="federation.SignatureAuth")
|
||||
class SignatureAuthFactory(factory.Factory):
|
||||
algorithm = 'rsa-sha256'
|
||||
algorithm = "rsa-sha256"
|
||||
key = factory.LazyFunction(lambda: keys.get_key_pair()[0])
|
||||
key_id = factory.Faker('url')
|
||||
key_id = factory.Faker("url")
|
||||
use_auth_header = False
|
||||
headers = [
|
||||
'(request-target)',
|
||||
'user-agent',
|
||||
'host',
|
||||
'date',
|
||||
'content-type',]
|
||||
headers = ["(request-target)", "user-agent", "host", "date", "content-type"]
|
||||
|
||||
class Meta:
|
||||
model = requests_http_signature.HTTPSignatureAuth
|
||||
|
||||
|
||||
@registry.register(name='federation.SignedRequest')
|
||||
@registry.register(name="federation.SignedRequest")
|
||||
class SignedRequestFactory(factory.Factory):
|
||||
url = factory.Faker('url')
|
||||
method = 'get'
|
||||
url = factory.Faker("url")
|
||||
method = "get"
|
||||
auth = factory.SubFactory(SignatureAuthFactory)
|
||||
|
||||
class Meta:
|
||||
|
@ -43,59 +39,62 @@ class SignedRequestFactory(factory.Factory):
|
|||
@factory.post_generation
|
||||
def headers(self, create, extracted, **kwargs):
|
||||
default_headers = {
|
||||
'User-Agent': 'Test',
|
||||
'Host': 'test.host',
|
||||
'Date': 'Right now',
|
||||
'Content-Type': 'application/activity+json'
|
||||
"User-Agent": "Test",
|
||||
"Host": "test.host",
|
||||
"Date": "Right now",
|
||||
"Content-Type": "application/activity+json",
|
||||
}
|
||||
if extracted:
|
||||
default_headers.update(extracted)
|
||||
self.headers.update(default_headers)
|
||||
|
||||
|
||||
@registry.register(name='federation.Link')
|
||||
@registry.register(name="federation.Link")
|
||||
class LinkFactory(factory.Factory):
|
||||
type = 'Link'
|
||||
href = factory.Faker('url')
|
||||
mediaType = 'text/html'
|
||||
type = "Link"
|
||||
href = factory.Faker("url")
|
||||
mediaType = "text/html"
|
||||
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
class Params:
|
||||
audio = factory.Trait(
|
||||
mediaType=factory.Iterator(['audio/mp3', 'audio/ogg'])
|
||||
)
|
||||
audio = factory.Trait(mediaType=factory.Iterator(["audio/mp3", "audio/ogg"]))
|
||||
|
||||
|
||||
@registry.register
|
||||
class ActorFactory(factory.DjangoModelFactory):
|
||||
public_key = None
|
||||
private_key = None
|
||||
preferred_username = factory.Faker('user_name')
|
||||
summary = factory.Faker('paragraph')
|
||||
domain = factory.Faker('domain_name')
|
||||
url = factory.LazyAttribute(lambda o: 'https://{}/users/{}'.format(o.domain, o.preferred_username))
|
||||
inbox_url = factory.LazyAttribute(lambda o: 'https://{}/users/{}/inbox'.format(o.domain, o.preferred_username))
|
||||
outbox_url = factory.LazyAttribute(lambda o: 'https://{}/users/{}/outbox'.format(o.domain, o.preferred_username))
|
||||
preferred_username = factory.Faker("user_name")
|
||||
summary = factory.Faker("paragraph")
|
||||
domain = factory.Faker("domain_name")
|
||||
url = factory.LazyAttribute(
|
||||
lambda o: "https://{}/users/{}".format(o.domain, o.preferred_username)
|
||||
)
|
||||
inbox_url = factory.LazyAttribute(
|
||||
lambda o: "https://{}/users/{}/inbox".format(o.domain, o.preferred_username)
|
||||
)
|
||||
outbox_url = factory.LazyAttribute(
|
||||
lambda o: "https://{}/users/{}/outbox".format(o.domain, o.preferred_username)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.Actor
|
||||
|
||||
class Params:
|
||||
local = factory.Trait(
|
||||
domain=factory.LazyAttribute(
|
||||
lambda o: settings.FEDERATION_HOSTNAME)
|
||||
domain=factory.LazyAttribute(lambda o: settings.FEDERATION_HOSTNAME)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate(cls, create, attrs):
|
||||
has_public = attrs.get('public_key') is not None
|
||||
has_private = attrs.get('private_key') is not None
|
||||
has_public = attrs.get("public_key") is not None
|
||||
has_private = attrs.get("private_key") is not None
|
||||
if not has_public and not has_private:
|
||||
private, public = keys.get_key_pair()
|
||||
attrs['private_key'] = private.decode('utf-8')
|
||||
attrs['public_key'] = public.decode('utf-8')
|
||||
attrs["private_key"] = private.decode("utf-8")
|
||||
attrs["public_key"] = public.decode("utf-8")
|
||||
return super()._generate(create, attrs)
|
||||
|
||||
|
||||
|
@ -108,15 +107,13 @@ class FollowFactory(factory.DjangoModelFactory):
|
|||
model = models.Follow
|
||||
|
||||
class Params:
|
||||
local = factory.Trait(
|
||||
actor=factory.SubFactory(ActorFactory, local=True)
|
||||
)
|
||||
local = factory.Trait(actor=factory.SubFactory(ActorFactory, local=True))
|
||||
|
||||
|
||||
@registry.register
|
||||
class LibraryFactory(factory.DjangoModelFactory):
|
||||
actor = factory.SubFactory(ActorFactory)
|
||||
url = factory.Faker('url')
|
||||
url = factory.Faker("url")
|
||||
federation_enabled = True
|
||||
download_files = False
|
||||
autoimport = False
|
||||
|
@ -126,42 +123,36 @@ class LibraryFactory(factory.DjangoModelFactory):
|
|||
|
||||
|
||||
class ArtistMetadataFactory(factory.Factory):
|
||||
name = factory.Faker('name')
|
||||
name = factory.Faker("name")
|
||||
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
class Params:
|
||||
musicbrainz = factory.Trait(
|
||||
musicbrainz_id=factory.Faker('uuid4')
|
||||
)
|
||||
musicbrainz = factory.Trait(musicbrainz_id=factory.Faker("uuid4"))
|
||||
|
||||
|
||||
class ReleaseMetadataFactory(factory.Factory):
|
||||
title = factory.Faker('sentence')
|
||||
title = factory.Faker("sentence")
|
||||
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
class Params:
|
||||
musicbrainz = factory.Trait(
|
||||
musicbrainz_id=factory.Faker('uuid4')
|
||||
)
|
||||
musicbrainz = factory.Trait(musicbrainz_id=factory.Faker("uuid4"))
|
||||
|
||||
|
||||
class RecordingMetadataFactory(factory.Factory):
|
||||
title = factory.Faker('sentence')
|
||||
title = factory.Faker("sentence")
|
||||
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
class Params:
|
||||
musicbrainz = factory.Trait(
|
||||
musicbrainz_id=factory.Faker('uuid4')
|
||||
)
|
||||
musicbrainz = factory.Trait(musicbrainz_id=factory.Faker("uuid4"))
|
||||
|
||||
|
||||
@registry.register(name='federation.LibraryTrackMetadata')
|
||||
@registry.register(name="federation.LibraryTrackMetadata")
|
||||
class LibraryTrackMetadataFactory(factory.Factory):
|
||||
artist = factory.SubFactory(ArtistMetadataFactory)
|
||||
recording = factory.SubFactory(RecordingMetadataFactory)
|
||||
|
@ -174,64 +165,59 @@ class LibraryTrackMetadataFactory(factory.Factory):
|
|||
@registry.register
|
||||
class LibraryTrackFactory(factory.DjangoModelFactory):
|
||||
library = factory.SubFactory(LibraryFactory)
|
||||
url = factory.Faker('url')
|
||||
title = factory.Faker('sentence')
|
||||
artist_name = factory.Faker('sentence')
|
||||
album_title = factory.Faker('sentence')
|
||||
audio_url = factory.Faker('url')
|
||||
audio_mimetype = 'audio/ogg'
|
||||
url = factory.Faker("url")
|
||||
title = factory.Faker("sentence")
|
||||
artist_name = factory.Faker("sentence")
|
||||
album_title = factory.Faker("sentence")
|
||||
audio_url = factory.Faker("url")
|
||||
audio_mimetype = "audio/ogg"
|
||||
metadata = factory.SubFactory(LibraryTrackMetadataFactory)
|
||||
|
||||
class Meta:
|
||||
model = models.LibraryTrack
|
||||
|
||||
class Params:
|
||||
with_audio_file = factory.Trait(
|
||||
audio_file=factory.django.FileField()
|
||||
)
|
||||
with_audio_file = factory.Trait(audio_file=factory.django.FileField())
|
||||
|
||||
|
||||
@registry.register(name='federation.Note')
|
||||
@registry.register(name="federation.Note")
|
||||
class NoteFactory(factory.Factory):
|
||||
type = 'Note'
|
||||
id = factory.Faker('url')
|
||||
published = factory.LazyFunction(
|
||||
lambda: timezone.now().isoformat()
|
||||
)
|
||||
type = "Note"
|
||||
id = factory.Faker("url")
|
||||
published = factory.LazyFunction(lambda: timezone.now().isoformat())
|
||||
inReplyTo = None
|
||||
content = factory.Faker('sentence')
|
||||
content = factory.Faker("sentence")
|
||||
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
|
||||
@registry.register(name='federation.Activity')
|
||||
@registry.register(name="federation.Activity")
|
||||
class ActivityFactory(factory.Factory):
|
||||
type = 'Create'
|
||||
id = factory.Faker('url')
|
||||
published = factory.LazyFunction(
|
||||
lambda: timezone.now().isoformat()
|
||||
)
|
||||
actor = factory.Faker('url')
|
||||
type = "Create"
|
||||
id = factory.Faker("url")
|
||||
published = factory.LazyFunction(lambda: timezone.now().isoformat())
|
||||
actor = factory.Faker("url")
|
||||
object = factory.SubFactory(
|
||||
NoteFactory,
|
||||
actor=factory.SelfAttribute('..actor'),
|
||||
published=factory.SelfAttribute('..published'))
|
||||
actor=factory.SelfAttribute("..actor"),
|
||||
published=factory.SelfAttribute("..published"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = dict
|
||||
|
||||
|
||||
@registry.register(name='federation.AudioMetadata')
|
||||
@registry.register(name="federation.AudioMetadata")
|
||||
class AudioMetadataFactory(factory.Factory):
|
||||
recording = factory.LazyAttribute(
|
||||
lambda o: 'https://musicbrainz.org/recording/{}'.format(uuid.uuid4())
|
||||
lambda o: "https://musicbrainz.org/recording/{}".format(uuid.uuid4())
|
||||
)
|
||||
artist = factory.LazyAttribute(
|
||||
lambda o: 'https://musicbrainz.org/artist/{}'.format(uuid.uuid4())
|
||||
lambda o: "https://musicbrainz.org/artist/{}".format(uuid.uuid4())
|
||||
)
|
||||
release = factory.LazyAttribute(
|
||||
lambda o: 'https://musicbrainz.org/release/{}'.format(uuid.uuid4())
|
||||
lambda o: "https://musicbrainz.org/release/{}".format(uuid.uuid4())
|
||||
)
|
||||
bitrate = 42
|
||||
length = 43
|
||||
|
@ -241,14 +227,12 @@ class AudioMetadataFactory(factory.Factory):
|
|||
model = dict
|
||||
|
||||
|
||||
@registry.register(name='federation.Audio')
|
||||
@registry.register(name="federation.Audio")
|
||||
class AudioFactory(factory.Factory):
|
||||
type = 'Audio'
|
||||
id = factory.Faker('url')
|
||||
published = factory.LazyFunction(
|
||||
lambda: timezone.now().isoformat()
|
||||
)
|
||||
actor = factory.Faker('url')
|
||||
type = "Audio"
|
||||
id = factory.Faker("url")
|
||||
published = factory.LazyFunction(lambda: timezone.now().isoformat())
|
||||
actor = factory.Faker("url")
|
||||
url = factory.SubFactory(LinkFactory, audio=True)
|
||||
metadata = factory.SubFactory(LibraryTrackMetadataFactory)
|
||||
|
||||
|
|
|
@ -6,73 +6,67 @@ from . import models
|
|||
|
||||
|
||||
class LibraryFilter(django_filters.FilterSet):
|
||||
approved = django_filters.BooleanFilter('following__approved')
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'actor__domain',
|
||||
])
|
||||
approved = django_filters.BooleanFilter("following__approved")
|
||||
q = fields.SearchFilter(search_fields=["actor__domain"])
|
||||
|
||||
class Meta:
|
||||
model = models.Library
|
||||
fields = {
|
||||
'approved': ['exact'],
|
||||
'federation_enabled': ['exact'],
|
||||
'download_files': ['exact'],
|
||||
'autoimport': ['exact'],
|
||||
'tracks_count': ['exact'],
|
||||
"approved": ["exact"],
|
||||
"federation_enabled": ["exact"],
|
||||
"download_files": ["exact"],
|
||||
"autoimport": ["exact"],
|
||||
"tracks_count": ["exact"],
|
||||
}
|
||||
|
||||
|
||||
class LibraryTrackFilter(django_filters.FilterSet):
|
||||
library = django_filters.CharFilter('library__uuid')
|
||||
status = django_filters.CharFilter(method='filter_status')
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'artist_name',
|
||||
'title',
|
||||
'album_title',
|
||||
'library__actor__domain',
|
||||
])
|
||||
library = django_filters.CharFilter("library__uuid")
|
||||
status = django_filters.CharFilter(method="filter_status")
|
||||
q = fields.SearchFilter(
|
||||
search_fields=["artist_name", "title", "album_title", "library__actor__domain"]
|
||||
)
|
||||
|
||||
def filter_status(self, queryset, field_name, value):
|
||||
if value == 'imported':
|
||||
if value == "imported":
|
||||
return queryset.filter(local_track_file__isnull=False)
|
||||
elif value == 'not_imported':
|
||||
return queryset.filter(
|
||||
local_track_file__isnull=True
|
||||
).exclude(import_jobs__status='pending')
|
||||
elif value == 'import_pending':
|
||||
return queryset.filter(import_jobs__status='pending')
|
||||
elif value == "not_imported":
|
||||
return queryset.filter(local_track_file__isnull=True).exclude(
|
||||
import_jobs__status="pending"
|
||||
)
|
||||
elif value == "import_pending":
|
||||
return queryset.filter(import_jobs__status="pending")
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = models.LibraryTrack
|
||||
fields = {
|
||||
'library': ['exact'],
|
||||
'artist_name': ['exact', 'icontains'],
|
||||
'title': ['exact', 'icontains'],
|
||||
'album_title': ['exact', 'icontains'],
|
||||
'audio_mimetype': ['exact', 'icontains'],
|
||||
"library": ["exact"],
|
||||
"artist_name": ["exact", "icontains"],
|
||||
"title": ["exact", "icontains"],
|
||||
"album_title": ["exact", "icontains"],
|
||||
"audio_mimetype": ["exact", "icontains"],
|
||||
}
|
||||
|
||||
|
||||
class FollowFilter(django_filters.FilterSet):
|
||||
pending = django_filters.CharFilter(method='filter_pending')
|
||||
pending = django_filters.CharFilter(method="filter_pending")
|
||||
ordering = django_filters.OrderingFilter(
|
||||
# tuple-mapping retains order
|
||||
fields=(
|
||||
('creation_date', 'creation_date'),
|
||||
('modification_date', 'modification_date'),
|
||||
),
|
||||
("creation_date", "creation_date"),
|
||||
("modification_date", "modification_date"),
|
||||
)
|
||||
)
|
||||
q = fields.SearchFilter(
|
||||
search_fields=["actor__domain", "actor__preferred_username"]
|
||||
)
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'actor__domain',
|
||||
'actor__preferred_username',
|
||||
])
|
||||
|
||||
class Meta:
|
||||
model = models.Follow
|
||||
fields = ['approved', 'pending', 'q']
|
||||
fields = ["approved", "pending", "q"]
|
||||
|
||||
def filter_pending(self, queryset, field_name, value):
|
||||
if value.lower() in ['true', '1', 'yes']:
|
||||
if value.lower() in ["true", "1", "yes"]:
|
||||
queryset = queryset.filter(approved__isnull=True)
|
||||
return queryset
|
||||
|
|
|
@ -7,42 +7,40 @@ import urllib.parse
|
|||
|
||||
from . import exceptions
|
||||
|
||||
KEY_ID_REGEX = re.compile(r'keyId=\"(?P<id>.*)\"')
|
||||
KEY_ID_REGEX = re.compile(r"keyId=\"(?P<id>.*)\"")
|
||||
|
||||
|
||||
def get_key_pair(size=2048):
|
||||
key = rsa.generate_private_key(
|
||||
backend=crypto_default_backend(),
|
||||
public_exponent=65537,
|
||||
key_size=size
|
||||
backend=crypto_default_backend(), public_exponent=65537, key_size=size
|
||||
)
|
||||
private_key = key.private_bytes(
|
||||
crypto_serialization.Encoding.PEM,
|
||||
crypto_serialization.PrivateFormat.PKCS8,
|
||||
crypto_serialization.NoEncryption())
|
||||
crypto_serialization.NoEncryption(),
|
||||
)
|
||||
public_key = key.public_key().public_bytes(
|
||||
crypto_serialization.Encoding.PEM,
|
||||
crypto_serialization.PublicFormat.PKCS1
|
||||
crypto_serialization.Encoding.PEM, crypto_serialization.PublicFormat.PKCS1
|
||||
)
|
||||
|
||||
return private_key, public_key
|
||||
|
||||
|
||||
def get_key_id_from_signature_header(header_string):
|
||||
parts = header_string.split(',')
|
||||
parts = header_string.split(",")
|
||||
try:
|
||||
raw_key_id = [p for p in parts if p.startswith('keyId="')][0]
|
||||
except IndexError:
|
||||
raise ValueError('Missing key id')
|
||||
raise ValueError("Missing key id")
|
||||
|
||||
match = KEY_ID_REGEX.match(raw_key_id)
|
||||
if not match:
|
||||
raise ValueError('Invalid key id')
|
||||
raise ValueError("Invalid key id")
|
||||
|
||||
key_id = match.groups()[0]
|
||||
url = urllib.parse.urlparse(key_id)
|
||||
if not url.scheme or not url.netloc:
|
||||
raise ValueError('Invalid url')
|
||||
if url.scheme not in ['http', 'https']:
|
||||
raise ValueError('Invalid shceme')
|
||||
raise ValueError("Invalid url")
|
||||
if url.scheme not in ["http", "https"]:
|
||||
raise ValueError("Invalid shceme")
|
||||
return key_id
|
||||
|
|
|
@ -24,87 +24,66 @@ def scan_from_account_name(account_name):
|
|||
"""
|
||||
data = {}
|
||||
try:
|
||||
username, domain = webfinger.clean_acct(
|
||||
account_name, ensure_local=False)
|
||||
username, domain = webfinger.clean_acct(account_name, ensure_local=False)
|
||||
except serializers.ValidationError:
|
||||
return {
|
||||
'webfinger': {
|
||||
'errors': ['Invalid account string']
|
||||
}
|
||||
}
|
||||
system_library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||
library = models.Library.objects.filter(
|
||||
actor__domain=domain,
|
||||
actor__preferred_username=username
|
||||
).select_related('actor').first()
|
||||
data['local'] = {
|
||||
'following': False,
|
||||
'awaiting_approval': False,
|
||||
}
|
||||
return {"webfinger": {"errors": ["Invalid account string"]}}
|
||||
system_library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
||||
library = (
|
||||
models.Library.objects.filter(
|
||||
actor__domain=domain, actor__preferred_username=username
|
||||
)
|
||||
.select_related("actor")
|
||||
.first()
|
||||
)
|
||||
data["local"] = {"following": False, "awaiting_approval": False}
|
||||
try:
|
||||
follow = models.Follow.objects.get(
|
||||
target__preferred_username=username,
|
||||
target__domain=username,
|
||||
actor=system_library,
|
||||
)
|
||||
data['local']['awaiting_approval'] = not bool(follow.approved)
|
||||
data['local']['following'] = True
|
||||
data["local"]["awaiting_approval"] = not bool(follow.approved)
|
||||
data["local"]["following"] = True
|
||||
except models.Follow.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
data['webfinger'] = webfinger.get_resource(
|
||||
'acct:{}'.format(account_name))
|
||||
data["webfinger"] = webfinger.get_resource("acct:{}".format(account_name))
|
||||
except requests.ConnectionError:
|
||||
return {
|
||||
'webfinger': {
|
||||
'errors': ['This webfinger resource is not reachable']
|
||||
}
|
||||
}
|
||||
return {"webfinger": {"errors": ["This webfinger resource is not reachable"]}}
|
||||
except requests.HTTPError as e:
|
||||
return {
|
||||
'webfinger': {
|
||||
'errors': [
|
||||
'Error {} during webfinger request'.format(
|
||||
e.response.status_code)]
|
||||
"webfinger": {
|
||||
"errors": [
|
||||
"Error {} during webfinger request".format(e.response.status_code)
|
||||
]
|
||||
}
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
return {
|
||||
'webfinger': {
|
||||
'errors': ['Could not process webfinger response']
|
||||
}
|
||||
}
|
||||
return {"webfinger": {"errors": ["Could not process webfinger response"]}}
|
||||
|
||||
try:
|
||||
data['actor'] = actors.get_actor_data(data['webfinger']['actor_url'])
|
||||
data["actor"] = actors.get_actor_data(data["webfinger"]["actor_url"])
|
||||
except requests.ConnectionError:
|
||||
data['actor'] = {
|
||||
'errors': ['This actor is not reachable']
|
||||
}
|
||||
data["actor"] = {"errors": ["This actor is not reachable"]}
|
||||
return data
|
||||
except requests.HTTPError as e:
|
||||
data['actor'] = {
|
||||
'errors': [
|
||||
'Error {} during actor request'.format(
|
||||
e.response.status_code)]
|
||||
data["actor"] = {
|
||||
"errors": ["Error {} during actor request".format(e.response.status_code)]
|
||||
}
|
||||
return data
|
||||
|
||||
serializer = serializers.LibraryActorSerializer(data=data['actor'])
|
||||
serializer = serializers.LibraryActorSerializer(data=data["actor"])
|
||||
if not serializer.is_valid():
|
||||
data['actor'] = {
|
||||
'errors': ['Invalid ActivityPub actor']
|
||||
}
|
||||
data["actor"] = {"errors": ["Invalid ActivityPub actor"]}
|
||||
return data
|
||||
data['library'] = get_library_data(
|
||||
serializer.validated_data['library_url'])
|
||||
data["library"] = get_library_data(serializer.validated_data["library_url"])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_library_data(library_url):
|
||||
actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||
actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
||||
auth = signing.get_auth(actor.private_key, actor.private_key_id)
|
||||
try:
|
||||
response = session.get_session().get(
|
||||
|
@ -112,55 +91,37 @@ def get_library_data(library_url):
|
|||
auth=auth,
|
||||
timeout=5,
|
||||
verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
|
||||
headers={
|
||||
'Content-Type': 'application/activity+json'
|
||||
}
|
||||
headers={"Content-Type": "application/activity+json"},
|
||||
)
|
||||
except requests.ConnectionError:
|
||||
return {
|
||||
'errors': ['This library is not reachable']
|
||||
}
|
||||
return {"errors": ["This library is not reachable"]}
|
||||
scode = response.status_code
|
||||
if scode == 401:
|
||||
return {
|
||||
'errors': ['This library requires authentication']
|
||||
}
|
||||
return {"errors": ["This library requires authentication"]}
|
||||
elif scode == 403:
|
||||
return {
|
||||
'errors': ['Permission denied while scanning library']
|
||||
}
|
||||
return {"errors": ["Permission denied while scanning library"]}
|
||||
elif scode >= 400:
|
||||
return {
|
||||
'errors': ['Error {} while fetching the library'.format(scode)]
|
||||
}
|
||||
serializer = serializers.PaginatedCollectionSerializer(
|
||||
data=response.json(),
|
||||
)
|
||||
return {"errors": ["Error {} while fetching the library".format(scode)]}
|
||||
serializer = serializers.PaginatedCollectionSerializer(data=response.json())
|
||||
if not serializer.is_valid():
|
||||
return {
|
||||
'errors': [
|
||||
'Invalid ActivityPub response from remote library']
|
||||
}
|
||||
return {"errors": ["Invalid ActivityPub response from remote library"]}
|
||||
|
||||
return serializer.validated_data
|
||||
|
||||
|
||||
def get_library_page(library, page_url):
|
||||
actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||
actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
||||
auth = signing.get_auth(actor.private_key, actor.private_key_id)
|
||||
response = session.get_session().get(
|
||||
page_url,
|
||||
auth=auth,
|
||||
timeout=5,
|
||||
verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
|
||||
headers={
|
||||
'Content-Type': 'application/activity+json'
|
||||
}
|
||||
headers={"Content-Type": "application/activity+json"},
|
||||
)
|
||||
serializer = serializers.CollectionPageSerializer(
|
||||
data=response.json(),
|
||||
context={
|
||||
'library': library,
|
||||
'item_serializer': serializers.AudioSerializer})
|
||||
context={"library": library, "item_serializer": serializers.AudioSerializer},
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer.validated_data
|
||||
|
|
|
@ -8,30 +8,74 @@ class Migration(migrations.Migration):
|
|||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Actor',
|
||||
name="Actor",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('url', models.URLField(db_index=True, max_length=500, unique=True)),
|
||||
('outbox_url', models.URLField(max_length=500)),
|
||||
('inbox_url', models.URLField(max_length=500)),
|
||||
('following_url', models.URLField(blank=True, max_length=500, null=True)),
|
||||
('followers_url', models.URLField(blank=True, max_length=500, null=True)),
|
||||
('shared_inbox_url', models.URLField(blank=True, max_length=500, null=True)),
|
||||
('type', models.CharField(choices=[('Person', 'Person'), ('Application', 'Application'), ('Group', 'Group'), ('Organization', 'Organization'), ('Service', 'Service')], default='Person', max_length=25)),
|
||||
('name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('domain', models.CharField(max_length=1000)),
|
||||
('summary', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('preferred_username', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('public_key', models.CharField(blank=True, max_length=5000, null=True)),
|
||||
('private_key', models.CharField(blank=True, max_length=5000, null=True)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('last_fetch_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('manually_approves_followers', models.NullBooleanField(default=None)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("url", models.URLField(db_index=True, max_length=500, unique=True)),
|
||||
("outbox_url", models.URLField(max_length=500)),
|
||||
("inbox_url", models.URLField(max_length=500)),
|
||||
(
|
||||
"following_url",
|
||||
models.URLField(blank=True, max_length=500, null=True),
|
||||
),
|
||||
(
|
||||
"followers_url",
|
||||
models.URLField(blank=True, max_length=500, null=True),
|
||||
),
|
||||
(
|
||||
"shared_inbox_url",
|
||||
models.URLField(blank=True, max_length=500, null=True),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("Person", "Person"),
|
||||
("Application", "Application"),
|
||||
("Group", "Group"),
|
||||
("Organization", "Organization"),
|
||||
("Service", "Service"),
|
||||
],
|
||||
default="Person",
|
||||
max_length=25,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(blank=True, max_length=200, null=True)),
|
||||
("domain", models.CharField(max_length=1000)),
|
||||
("summary", models.CharField(blank=True, max_length=500, null=True)),
|
||||
(
|
||||
"preferred_username",
|
||||
models.CharField(blank=True, max_length=200, null=True),
|
||||
),
|
||||
(
|
||||
"public_key",
|
||||
models.CharField(blank=True, max_length=5000, null=True),
|
||||
),
|
||||
(
|
||||
"private_key",
|
||||
models.CharField(blank=True, max_length=5000, null=True),
|
||||
),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
(
|
||||
"last_fetch_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("manually_approves_followers", models.NullBooleanField(default=None)),
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
|
|
|
@ -5,13 +5,10 @@ from django.db import migrations
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('federation', '0001_initial'),
|
||||
]
|
||||
dependencies = [("federation", "0001_initial")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='actor',
|
||||
unique_together={('domain', 'preferred_username')},
|
||||
),
|
||||
name="actor", unique_together={("domain", "preferred_username")}
|
||||
)
|
||||
]
|
||||
|
|
|
@ -10,7 +10,7 @@ import uuid
|
|||
def delete_system_actors(apps, schema_editor):
|
||||
"""Revert site domain and name to default."""
|
||||
Actor = apps.get_model("federation", "Actor")
|
||||
Actor.objects.filter(preferred_username__in=['test', 'library']).delete()
|
||||
Actor.objects.filter(preferred_username__in=["test", "library"]).delete()
|
||||
|
||||
|
||||
def backward(apps, schema_editor):
|
||||
|
@ -19,76 +19,168 @@ def backward(apps, schema_editor):
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('federation', '0002_auto_20180403_1620'),
|
||||
]
|
||||
dependencies = [("federation", "0002_auto_20180403_1620")]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(delete_system_actors, backward),
|
||||
migrations.CreateModel(
|
||||
name='Follow',
|
||||
name="Follow",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('modification_date', models.DateTimeField(auto_now=True)),
|
||||
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emitted_follows', to='federation.Actor')),
|
||||
('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follows', to='federation.Actor')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("uuid", models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("modification_date", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"actor",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="emitted_follows",
|
||||
to="federation.Actor",
|
||||
),
|
||||
),
|
||||
(
|
||||
"target",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="received_follows",
|
||||
to="federation.Actor",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FollowRequest',
|
||||
name="FollowRequest",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('modification_date', models.DateTimeField(auto_now=True)),
|
||||
('approved', models.NullBooleanField(default=None)),
|
||||
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emmited_follow_requests', to='federation.Actor')),
|
||||
('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follow_requests', to='federation.Actor')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("uuid", models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("modification_date", models.DateTimeField(auto_now=True)),
|
||||
("approved", models.NullBooleanField(default=None)),
|
||||
(
|
||||
"actor",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="emmited_follow_requests",
|
||||
to="federation.Actor",
|
||||
),
|
||||
),
|
||||
(
|
||||
"target",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="received_follow_requests",
|
||||
to="federation.Actor",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Library',
|
||||
name="Library",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('modification_date', models.DateTimeField(auto_now=True)),
|
||||
('fetched_date', models.DateTimeField(blank=True, null=True)),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4)),
|
||||
('url', models.URLField()),
|
||||
('federation_enabled', models.BooleanField()),
|
||||
('download_files', models.BooleanField()),
|
||||
('autoimport', models.BooleanField()),
|
||||
('tracks_count', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('actor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='library', to='federation.Actor')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("modification_date", models.DateTimeField(auto_now=True)),
|
||||
("fetched_date", models.DateTimeField(blank=True, null=True)),
|
||||
("uuid", models.UUIDField(default=uuid.uuid4)),
|
||||
("url", models.URLField()),
|
||||
("federation_enabled", models.BooleanField()),
|
||||
("download_files", models.BooleanField()),
|
||||
("autoimport", models.BooleanField()),
|
||||
("tracks_count", models.PositiveIntegerField(blank=True, null=True)),
|
||||
(
|
||||
"actor",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="library",
|
||||
to="federation.Actor",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LibraryTrack',
|
||||
name="LibraryTrack",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('url', models.URLField(unique=True)),
|
||||
('audio_url', models.URLField()),
|
||||
('audio_mimetype', models.CharField(max_length=200)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('modification_date', models.DateTimeField(auto_now=True)),
|
||||
('fetched_date', models.DateTimeField(blank=True, null=True)),
|
||||
('published_date', models.DateTimeField(blank=True, null=True)),
|
||||
('artist_name', models.CharField(max_length=500)),
|
||||
('album_title', models.CharField(max_length=500)),
|
||||
('title', models.CharField(max_length=500)),
|
||||
('metadata', django.contrib.postgres.fields.jsonb.JSONField(default={}, max_length=10000)),
|
||||
('library', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracks', to='federation.Library')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("url", models.URLField(unique=True)),
|
||||
("audio_url", models.URLField()),
|
||||
("audio_mimetype", models.CharField(max_length=200)),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("modification_date", models.DateTimeField(auto_now=True)),
|
||||
("fetched_date", models.DateTimeField(blank=True, null=True)),
|
||||
("published_date", models.DateTimeField(blank=True, null=True)),
|
||||
("artist_name", models.CharField(max_length=500)),
|
||||
("album_title", models.CharField(max_length=500)),
|
||||
("title", models.CharField(max_length=500)),
|
||||
(
|
||||
"metadata",
|
||||
django.contrib.postgres.fields.jsonb.JSONField(
|
||||
default={}, max_length=10000
|
||||
),
|
||||
),
|
||||
(
|
||||
"library",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="tracks",
|
||||
to="federation.Library",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='actor',
|
||||
name='followers',
|
||||
field=models.ManyToManyField(related_name='following', through='federation.Follow', to='federation.Actor'),
|
||||
model_name="actor",
|
||||
name="followers",
|
||||
field=models.ManyToManyField(
|
||||
related_name="following",
|
||||
through="federation.Follow",
|
||||
to="federation.Actor",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='follow',
|
||||
unique_together={('actor', 'target')},
|
||||
name="follow", unique_together={("actor", "target")}
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,30 +6,26 @@ import django.db.models.deletion
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('federation', '0003_auto_20180407_1010'),
|
||||
]
|
||||
dependencies = [("federation", "0003_auto_20180407_1010")]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='followrequest',
|
||||
name='actor',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='followrequest',
|
||||
name='target',
|
||||
),
|
||||
migrations.RemoveField(model_name="followrequest", name="actor"),
|
||||
migrations.RemoveField(model_name="followrequest", name="target"),
|
||||
migrations.AddField(
|
||||
model_name='follow',
|
||||
name='approved',
|
||||
model_name="follow",
|
||||
name="approved",
|
||||
field=models.NullBooleanField(default=None),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='library',
|
||||
name='follow',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='library', to='federation.Follow'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='FollowRequest',
|
||||
model_name="library",
|
||||
name="follow",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="library",
|
||||
to="federation.Follow",
|
||||
),
|
||||
),
|
||||
migrations.DeleteModel(name="FollowRequest"),
|
||||
]
|
||||
|
|
|
@ -8,19 +8,25 @@ import funkwhale_api.federation.models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('federation', '0004_auto_20180410_2025'),
|
||||
]
|
||||
dependencies = [("federation", "0004_auto_20180410_2025")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='librarytrack',
|
||||
name='audio_file',
|
||||
field=models.FileField(blank=True, null=True, upload_to=funkwhale_api.federation.models.get_file_path),
|
||||
model_name="librarytrack",
|
||||
name="audio_file",
|
||||
field=models.FileField(
|
||||
blank=True,
|
||||
null=True,
|
||||
upload_to=funkwhale_api.federation.models.get_file_path,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='librarytrack',
|
||||
name='metadata',
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(default={}, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=10000),
|
||||
model_name="librarytrack",
|
||||
name="metadata",
|
||||
field=django.contrib.postgres.fields.jsonb.JSONField(
|
||||
default={},
|
||||
encoder=django.core.serializers.json.DjangoJSONEncoder,
|
||||
max_length=10000,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -5,24 +5,20 @@ from django.db import migrations, models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('federation', '0005_auto_20180413_1723'),
|
||||
]
|
||||
dependencies = [("federation", "0005_auto_20180413_1723")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='library',
|
||||
name='url',
|
||||
model_name="library", name="url", field=models.URLField(max_length=500)
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="librarytrack",
|
||||
name="audio_url",
|
||||
field=models.URLField(max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='librarytrack',
|
||||
name='audio_url',
|
||||
field=models.URLField(max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='librarytrack',
|
||||
name='url',
|
||||
model_name="librarytrack",
|
||||
name="url",
|
||||
field=models.URLField(max_length=500, unique=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -12,16 +12,16 @@ from funkwhale_api.common import session
|
|||
from funkwhale_api.music import utils as music_utils
|
||||
|
||||
TYPE_CHOICES = [
|
||||
('Person', 'Person'),
|
||||
('Application', 'Application'),
|
||||
('Group', 'Group'),
|
||||
('Organization', 'Organization'),
|
||||
('Service', 'Service'),
|
||||
("Person", "Person"),
|
||||
("Application", "Application"),
|
||||
("Group", "Group"),
|
||||
("Organization", "Organization"),
|
||||
("Service", "Service"),
|
||||
]
|
||||
|
||||
|
||||
class Actor(models.Model):
|
||||
ap_type = 'Actor'
|
||||
ap_type = "Actor"
|
||||
|
||||
url = models.URLField(unique=True, max_length=500, db_index=True)
|
||||
outbox_url = models.URLField(max_length=500)
|
||||
|
@ -29,49 +29,41 @@ class Actor(models.Model):
|
|||
following_url = models.URLField(max_length=500, null=True, blank=True)
|
||||
followers_url = models.URLField(max_length=500, null=True, blank=True)
|
||||
shared_inbox_url = models.URLField(max_length=500, null=True, blank=True)
|
||||
type = models.CharField(
|
||||
choices=TYPE_CHOICES, default='Person', max_length=25)
|
||||
type = models.CharField(choices=TYPE_CHOICES, default="Person", max_length=25)
|
||||
name = models.CharField(max_length=200, null=True, blank=True)
|
||||
domain = models.CharField(max_length=1000)
|
||||
summary = models.CharField(max_length=500, null=True, blank=True)
|
||||
preferred_username = models.CharField(
|
||||
max_length=200, null=True, blank=True)
|
||||
preferred_username = models.CharField(max_length=200, null=True, blank=True)
|
||||
public_key = models.CharField(max_length=5000, null=True, blank=True)
|
||||
private_key = models.CharField(max_length=5000, null=True, blank=True)
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
last_fetch_date = models.DateTimeField(
|
||||
default=timezone.now)
|
||||
last_fetch_date = models.DateTimeField(default=timezone.now)
|
||||
manually_approves_followers = models.NullBooleanField(default=None)
|
||||
followers = models.ManyToManyField(
|
||||
to='self',
|
||||
to="self",
|
||||
symmetrical=False,
|
||||
through='Follow',
|
||||
through_fields=('target', 'actor'),
|
||||
related_name='following',
|
||||
through="Follow",
|
||||
through_fields=("target", "actor"),
|
||||
related_name="following",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['domain', 'preferred_username']
|
||||
unique_together = ["domain", "preferred_username"]
|
||||
|
||||
@property
|
||||
def webfinger_subject(self):
|
||||
return '{}@{}'.format(
|
||||
self.preferred_username,
|
||||
settings.FEDERATION_HOSTNAME,
|
||||
)
|
||||
return "{}@{}".format(self.preferred_username, settings.FEDERATION_HOSTNAME)
|
||||
|
||||
@property
|
||||
def private_key_id(self):
|
||||
return '{}#main-key'.format(self.url)
|
||||
return "{}#main-key".format(self.url)
|
||||
|
||||
@property
|
||||
def mention_username(self):
|
||||
return '@{}@{}'.format(self.preferred_username, self.domain)
|
||||
return "@{}@{}".format(self.preferred_username, self.domain)
|
||||
|
||||
def save(self, **kwargs):
|
||||
lowercase_fields = [
|
||||
'domain',
|
||||
]
|
||||
lowercase_fields = ["domain"]
|
||||
for field in lowercase_fields:
|
||||
v = getattr(self, field, None)
|
||||
if v:
|
||||
|
@ -86,58 +78,54 @@ class Actor(models.Model):
|
|||
@property
|
||||
def is_system(self):
|
||||
from . import actors
|
||||
return all([
|
||||
settings.FEDERATION_HOSTNAME == self.domain,
|
||||
self.preferred_username in actors.SYSTEM_ACTORS
|
||||
])
|
||||
|
||||
return all(
|
||||
[
|
||||
settings.FEDERATION_HOSTNAME == self.domain,
|
||||
self.preferred_username in actors.SYSTEM_ACTORS,
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def system_conf(self):
|
||||
from . import actors
|
||||
|
||||
if self.is_system:
|
||||
return actors.SYSTEM_ACTORS[self.preferred_username]
|
||||
|
||||
def get_approved_followers(self):
|
||||
follows = self.received_follows.filter(approved=True)
|
||||
return self.followers.filter(
|
||||
pk__in=follows.values_list('actor', flat=True))
|
||||
return self.followers.filter(pk__in=follows.values_list("actor", flat=True))
|
||||
|
||||
|
||||
class Follow(models.Model):
|
||||
ap_type = 'Follow'
|
||||
ap_type = "Follow"
|
||||
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
actor = models.ForeignKey(
|
||||
Actor,
|
||||
related_name='emitted_follows',
|
||||
on_delete=models.CASCADE,
|
||||
Actor, related_name="emitted_follows", on_delete=models.CASCADE
|
||||
)
|
||||
target = models.ForeignKey(
|
||||
Actor,
|
||||
related_name='received_follows',
|
||||
on_delete=models.CASCADE,
|
||||
Actor, related_name="received_follows", on_delete=models.CASCADE
|
||||
)
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
modification_date = models.DateTimeField(
|
||||
auto_now=True)
|
||||
modification_date = models.DateTimeField(auto_now=True)
|
||||
approved = models.NullBooleanField(default=None)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['actor', 'target']
|
||||
unique_together = ["actor", "target"]
|
||||
|
||||
def get_federation_url(self):
|
||||
return '{}#follows/{}'.format(self.actor.url, self.uuid)
|
||||
return "{}#follows/{}".format(self.actor.url, self.uuid)
|
||||
|
||||
|
||||
class Library(models.Model):
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
modification_date = models.DateTimeField(
|
||||
auto_now=True)
|
||||
modification_date = models.DateTimeField(auto_now=True)
|
||||
fetched_date = models.DateTimeField(null=True, blank=True)
|
||||
actor = models.OneToOneField(
|
||||
Actor,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='library')
|
||||
Actor, on_delete=models.CASCADE, related_name="library"
|
||||
)
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
url = models.URLField(max_length=500)
|
||||
|
||||
|
@ -149,69 +137,60 @@ class Library(models.Model):
|
|||
autoimport = models.BooleanField()
|
||||
tracks_count = models.PositiveIntegerField(null=True, blank=True)
|
||||
follow = models.OneToOneField(
|
||||
Follow,
|
||||
related_name='library',
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
Follow, related_name="library", null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
|
||||
def get_file_path(instance, filename):
|
||||
uid = str(uuid.uuid4())
|
||||
chunk_size = 2
|
||||
chunks = [uid[i:i+chunk_size] for i in range(0, len(uid), chunk_size)]
|
||||
chunks = [uid[i : i + chunk_size] for i in range(0, len(uid), chunk_size)]
|
||||
parts = chunks[:3] + [filename]
|
||||
return os.path.join('federation_cache', *parts)
|
||||
return os.path.join("federation_cache", *parts)
|
||||
|
||||
|
||||
class LibraryTrack(models.Model):
|
||||
url = models.URLField(unique=True, max_length=500)
|
||||
audio_url = models.URLField(max_length=500)
|
||||
audio_mimetype = models.CharField(max_length=200)
|
||||
audio_file = models.FileField(
|
||||
upload_to=get_file_path,
|
||||
null=True,
|
||||
blank=True)
|
||||
audio_file = models.FileField(upload_to=get_file_path, null=True, blank=True)
|
||||
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
modification_date = models.DateTimeField(
|
||||
auto_now=True)
|
||||
modification_date = models.DateTimeField(auto_now=True)
|
||||
fetched_date = models.DateTimeField(null=True, blank=True)
|
||||
published_date = models.DateTimeField(null=True, blank=True)
|
||||
library = models.ForeignKey(
|
||||
Library, related_name='tracks', on_delete=models.CASCADE)
|
||||
Library, related_name="tracks", on_delete=models.CASCADE
|
||||
)
|
||||
artist_name = models.CharField(max_length=500)
|
||||
album_title = models.CharField(max_length=500)
|
||||
title = models.CharField(max_length=500)
|
||||
metadata = JSONField(
|
||||
default={}, max_length=10000, encoder=DjangoJSONEncoder)
|
||||
metadata = JSONField(default={}, max_length=10000, encoder=DjangoJSONEncoder)
|
||||
|
||||
@property
|
||||
def mbid(self):
|
||||
try:
|
||||
return self.metadata['recording']['musicbrainz_id']
|
||||
return self.metadata["recording"]["musicbrainz_id"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def download_audio(self):
|
||||
from . import actors
|
||||
auth = actors.SYSTEM_ACTORS['library'].get_request_auth()
|
||||
|
||||
auth = actors.SYSTEM_ACTORS["library"].get_request_auth()
|
||||
remote_response = session.get_session().get(
|
||||
self.audio_url,
|
||||
auth=auth,
|
||||
stream=True,
|
||||
timeout=20,
|
||||
verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
|
||||
headers={
|
||||
'Content-Type': 'application/activity+json'
|
||||
}
|
||||
headers={"Content-Type": "application/activity+json"},
|
||||
)
|
||||
with remote_response as r:
|
||||
remote_response.raise_for_status()
|
||||
extension = music_utils.get_ext_from_type(self.audio_mimetype)
|
||||
title = ' - '.join([self.title, self.album_title, self.artist_name])
|
||||
filename = '{}.{}'.format(title, extension)
|
||||
title = " - ".join([self.title, self.album_title, self.artist_name])
|
||||
filename = "{}.{}".format(title, extension)
|
||||
tmp_file = tempfile.TemporaryFile()
|
||||
for chunk in r.iter_content(chunk_size=512):
|
||||
tmp_file.write(chunk)
|
||||
|
|
|
@ -2,4 +2,4 @@ from rest_framework import parsers
|
|||
|
||||
|
||||
class ActivityParser(parsers.JSONParser):
|
||||
media_type = 'application/activity+json'
|
||||
media_type = "application/activity+json"
|
||||
|
|
|
@ -7,15 +7,13 @@ from . import actors
|
|||
|
||||
|
||||
class LibraryFollower(BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not preferences.get('federation__music_needs_approval'):
|
||||
if not preferences.get("federation__music_needs_approval"):
|
||||
return True
|
||||
|
||||
actor = getattr(request, 'actor', None)
|
||||
actor = getattr(request, "actor", None)
|
||||
if actor is None:
|
||||
return False
|
||||
|
||||
library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||
return library.received_follows.filter(
|
||||
approved=True, actor=actor).exists()
|
||||
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
||||
return library.received_follows.filter(approved=True, actor=actor).exists()
|
||||
|
|
|
@ -2,8 +2,8 @@ from rest_framework.renderers import JSONRenderer
|
|||
|
||||
|
||||
class ActivityPubRenderer(JSONRenderer):
|
||||
media_type = 'application/activity+json'
|
||||
media_type = "application/activity+json"
|
||||
|
||||
|
||||
class WebfingerRenderer(JSONRenderer):
|
||||
media_type = 'application/jrd+json'
|
||||
media_type = "application/jrd+json"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -10,9 +10,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
def verify(request, public_key):
|
||||
return requests_http_signature.HTTPSignatureAuth.verify(
|
||||
request,
|
||||
key_resolver=lambda **kwargs: public_key,
|
||||
use_auth_header=False,
|
||||
request, key_resolver=lambda **kwargs: public_key, use_auth_header=False
|
||||
)
|
||||
|
||||
|
||||
|
@ -27,26 +25,24 @@ def verify_django(django_request, public_key):
|
|||
# with requests_http_signature
|
||||
headers[h.lower()] = v
|
||||
try:
|
||||
signature = headers['Signature']
|
||||
signature = headers["Signature"]
|
||||
except KeyError:
|
||||
raise exceptions.MissingSignature
|
||||
url = 'http://noop{}'.format(django_request.path)
|
||||
query = django_request.META['QUERY_STRING']
|
||||
url = "http://noop{}".format(django_request.path)
|
||||
query = django_request.META["QUERY_STRING"]
|
||||
if query:
|
||||
url += '?{}'.format(query)
|
||||
url += "?{}".format(query)
|
||||
signature_headers = signature.split('headers="')[1].split('",')[0]
|
||||
expected = signature_headers.split(' ')
|
||||
logger.debug('Signature expected headers: %s', expected)
|
||||
expected = signature_headers.split(" ")
|
||||
logger.debug("Signature expected headers: %s", expected)
|
||||
for header in expected:
|
||||
try:
|
||||
headers[header]
|
||||
except KeyError:
|
||||
logger.debug('Missing header: %s', header)
|
||||
logger.debug("Missing header: %s", header)
|
||||
request = requests.Request(
|
||||
method=django_request.method,
|
||||
url=url,
|
||||
data=django_request.body,
|
||||
headers=headers)
|
||||
method=django_request.method, url=url, data=django_request.body, headers=headers
|
||||
)
|
||||
for h in request.headers.keys():
|
||||
v = request.headers[h]
|
||||
if v:
|
||||
|
@ -58,13 +54,8 @@ def verify_django(django_request, public_key):
|
|||
def get_auth(private_key, private_key_id):
|
||||
return requests_http_signature.HTTPSignatureAuth(
|
||||
use_auth_header=False,
|
||||
headers=[
|
||||
'(request-target)',
|
||||
'user-agent',
|
||||
'host',
|
||||
'date',
|
||||
'content-type'],
|
||||
algorithm='rsa-sha256',
|
||||
key=private_key.encode('utf-8'),
|
||||
headers=["(request-target)", "user-agent", "host", "date", "content-type"],
|
||||
algorithm="rsa-sha256",
|
||||
key=private_key.encode("utf-8"),
|
||||
key_id=private_key_id,
|
||||
)
|
||||
|
|
|
@ -24,96 +24,100 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
@celery.app.task(
|
||||
name='federation.send',
|
||||
name="federation.send",
|
||||
autoretry_for=[RequestException],
|
||||
retry_backoff=30,
|
||||
max_retries=5)
|
||||
@celery.require_instance(models.Actor, 'actor')
|
||||
max_retries=5,
|
||||
)
|
||||
@celery.require_instance(models.Actor, "actor")
|
||||
def send(activity, actor, to):
|
||||
logger.info('Preparing activity delivery to %s', to)
|
||||
auth = signing.get_auth(
|
||||
actor.private_key, actor.private_key_id)
|
||||
logger.info("Preparing activity delivery to %s", to)
|
||||
auth = signing.get_auth(actor.private_key, actor.private_key_id)
|
||||
for url in to:
|
||||
recipient_actor = actors.get_actor(url)
|
||||
logger.debug('delivering to %s', recipient_actor.inbox_url)
|
||||
logger.debug('activity content: %s', json.dumps(activity))
|
||||
logger.debug("delivering to %s", recipient_actor.inbox_url)
|
||||
logger.debug("activity content: %s", json.dumps(activity))
|
||||
response = session.get_session().post(
|
||||
auth=auth,
|
||||
json=activity,
|
||||
url=recipient_actor.inbox_url,
|
||||
timeout=5,
|
||||
verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
|
||||
headers={
|
||||
'Content-Type': 'application/activity+json'
|
||||
}
|
||||
headers={"Content-Type": "application/activity+json"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
logger.debug('Remote answered with %s', response.status_code)
|
||||
logger.debug("Remote answered with %s", response.status_code)
|
||||
|
||||
|
||||
@celery.app.task(
|
||||
name='federation.scan_library',
|
||||
name="federation.scan_library",
|
||||
autoretry_for=[RequestException],
|
||||
retry_backoff=30,
|
||||
max_retries=5)
|
||||
@celery.require_instance(models.Library, 'library')
|
||||
max_retries=5,
|
||||
)
|
||||
@celery.require_instance(models.Library, "library")
|
||||
def scan_library(library, until=None):
|
||||
if not library.federation_enabled:
|
||||
return
|
||||
|
||||
data = lb.get_library_data(library.url)
|
||||
scan_library_page.delay(
|
||||
library_id=library.id, page_url=data['first'], until=until)
|
||||
scan_library_page.delay(library_id=library.id, page_url=data["first"], until=until)
|
||||
library.fetched_date = timezone.now()
|
||||
library.tracks_count = data['totalItems']
|
||||
library.save(update_fields=['fetched_date', 'tracks_count'])
|
||||
library.tracks_count = data["totalItems"]
|
||||
library.save(update_fields=["fetched_date", "tracks_count"])
|
||||
|
||||
|
||||
@celery.app.task(
|
||||
name='federation.scan_library_page',
|
||||
name="federation.scan_library_page",
|
||||
autoretry_for=[RequestException],
|
||||
retry_backoff=30,
|
||||
max_retries=5)
|
||||
@celery.require_instance(models.Library, 'library')
|
||||
max_retries=5,
|
||||
)
|
||||
@celery.require_instance(models.Library, "library")
|
||||
def scan_library_page(library, page_url, until=None):
|
||||
if not library.federation_enabled:
|
||||
return
|
||||
|
||||
data = lb.get_library_page(library, page_url)
|
||||
lts = []
|
||||
for item_serializer in data['items']:
|
||||
item_date = item_serializer.validated_data['published']
|
||||
for item_serializer in data["items"]:
|
||||
item_date = item_serializer.validated_data["published"]
|
||||
if until and item_date < until:
|
||||
return
|
||||
lts.append(item_serializer.save())
|
||||
|
||||
next_page = data.get('next')
|
||||
next_page = data.get("next")
|
||||
if next_page and next_page != page_url:
|
||||
scan_library_page.delay(library_id=library.id, page_url=next_page)
|
||||
|
||||
|
||||
@celery.app.task(name='federation.clean_music_cache')
|
||||
@celery.app.task(name="federation.clean_music_cache")
|
||||
def clean_music_cache():
|
||||
preferences = global_preferences_registry.manager()
|
||||
delay = preferences['federation__music_cache_duration']
|
||||
delay = preferences["federation__music_cache_duration"]
|
||||
if delay < 1:
|
||||
return # cache clearing disabled
|
||||
limit = timezone.now() - datetime.timedelta(minutes=delay)
|
||||
|
||||
candidates = models.LibraryTrack.objects.filter(
|
||||
Q(audio_file__isnull=False) & (
|
||||
Q(local_track_file__accessed_date__lt=limit) |
|
||||
Q(local_track_file__accessed_date=None)
|
||||
candidates = (
|
||||
models.LibraryTrack.objects.filter(
|
||||
Q(audio_file__isnull=False)
|
||||
& (
|
||||
Q(local_track_file__accessed_date__lt=limit)
|
||||
| Q(local_track_file__accessed_date=None)
|
||||
)
|
||||
)
|
||||
).exclude(audio_file='').only('audio_file', 'id')
|
||||
.exclude(audio_file="")
|
||||
.only("audio_file", "id")
|
||||
)
|
||||
for lt in candidates:
|
||||
lt.audio_file.delete()
|
||||
|
||||
# we also delete orphaned files, if any
|
||||
storage = models.LibraryTrack._meta.get_field('audio_file').storage
|
||||
files = get_files(storage, 'federation_cache')
|
||||
storage = models.LibraryTrack._meta.get_field("audio_file").storage
|
||||
files = get_files(storage, "federation_cache")
|
||||
existing = models.LibraryTrack.objects.filter(audio_file__in=files)
|
||||
missing = set(files) - set(existing.values_list('audio_file', flat=True))
|
||||
missing = set(files) - set(existing.values_list("audio_file", flat=True))
|
||||
for m in missing:
|
||||
storage.delete(m)
|
||||
|
||||
|
@ -124,12 +128,9 @@ def get_files(storage, *parts):
|
|||
in a given directory using django's storage.
|
||||
"""
|
||||
if not parts:
|
||||
raise ValueError('Missing path')
|
||||
raise ValueError("Missing path")
|
||||
|
||||
dirs, files = storage.listdir(os.path.join(*parts))
|
||||
for dir in dirs:
|
||||
files += get_files(storage, *(list(parts) + [dir]))
|
||||
return [
|
||||
os.path.join(parts[-1], path)
|
||||
for path in files
|
||||
]
|
||||
return [os.path.join(parts[-1], path) for path in files]
|
||||
|
|
|
@ -6,19 +6,11 @@ from . import views
|
|||
router = routers.SimpleRouter(trailing_slash=False)
|
||||
music_router = routers.SimpleRouter(trailing_slash=False)
|
||||
router.register(
|
||||
r'federation/instance/actors',
|
||||
views.InstanceActorViewSet,
|
||||
'instance-actors')
|
||||
router.register(
|
||||
r'.well-known',
|
||||
views.WellKnownViewSet,
|
||||
'well-known')
|
||||
|
||||
music_router.register(
|
||||
r'files',
|
||||
views.MusicFilesViewSet,
|
||||
'files',
|
||||
r"federation/instance/actors", views.InstanceActorViewSet, "instance-actors"
|
||||
)
|
||||
router.register(r".well-known", views.WellKnownViewSet, "well-known")
|
||||
|
||||
music_router.register(r"files", views.MusicFilesViewSet, "files")
|
||||
urlpatterns = router.urls + [
|
||||
url('federation/music/', include((music_router.urls, 'music'), namespace='music'))
|
||||
url("federation/music/", include((music_router.urls, "music"), namespace="music"))
|
||||
]
|
||||
|
|
|
@ -6,10 +6,10 @@ def full_url(path):
|
|||
Given a relative path, return a full url usable for federation purpose
|
||||
"""
|
||||
root = settings.FUNKWHALE_URL
|
||||
if path.startswith('/') and root.endswith('/'):
|
||||
if path.startswith("/") and root.endswith("/"):
|
||||
return root + path[1:]
|
||||
elif not path.startswith('/') and not root.endswith('/'):
|
||||
return root + '/' + path
|
||||
elif not path.startswith("/") and not root.endswith("/"):
|
||||
return root + "/" + path
|
||||
else:
|
||||
return root + path
|
||||
|
||||
|
@ -19,17 +19,14 @@ def clean_wsgi_headers(raw_headers):
|
|||
Convert WSGI headers from CONTENT_TYPE to Content-Type notation
|
||||
"""
|
||||
cleaned = {}
|
||||
non_prefixed = [
|
||||
'content_type',
|
||||
'content_length',
|
||||
]
|
||||
non_prefixed = ["content_type", "content_length"]
|
||||
for raw_header, value in raw_headers.items():
|
||||
h = raw_header.lower()
|
||||
if not h.startswith('http_') and h not in non_prefixed:
|
||||
if not h.startswith("http_") and h not in non_prefixed:
|
||||
continue
|
||||
|
||||
words = h.replace('http_', '', 1).split('_')
|
||||
cleaned_header = '-'.join([w.capitalize() for w in words])
|
||||
words = h.replace("http_", "", 1).split("_")
|
||||
cleaned_header = "-".join([w.capitalize() for w in words])
|
||||
cleaned[cleaned_header] = value
|
||||
|
||||
return cleaned
|
||||
|
|
|
@ -34,22 +34,21 @@ from . import webfinger
|
|||
|
||||
class FederationMixin(object):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not preferences.get('federation__enabled'):
|
||||
if not preferences.get("federation__enabled"):
|
||||
return HttpResponse(status=405)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
|
||||
lookup_field = 'actor'
|
||||
lookup_value_regex = '[a-z]*'
|
||||
authentication_classes = [
|
||||
authentication.SignatureAuthentication]
|
||||
lookup_field = "actor"
|
||||
lookup_value_regex = "[a-z]*"
|
||||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
permission_classes = []
|
||||
renderer_classes = [renderers.ActivityPubRenderer]
|
||||
|
||||
def get_object(self):
|
||||
try:
|
||||
return actors.SYSTEM_ACTORS[self.kwargs['actor']]
|
||||
return actors.SYSTEM_ACTORS[self.kwargs["actor"]]
|
||||
except KeyError:
|
||||
raise Http404
|
||||
|
||||
|
@ -59,12 +58,10 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
|
|||
data = actor.system_conf.serialize()
|
||||
return response.Response(data, status=200)
|
||||
|
||||
@detail_route(methods=['get', 'post'])
|
||||
@detail_route(methods=["get", "post"])
|
||||
def inbox(self, request, *args, **kwargs):
|
||||
system_actor = self.get_object()
|
||||
handler = getattr(system_actor, '{}_inbox'.format(
|
||||
request.method.lower()
|
||||
))
|
||||
handler = getattr(system_actor, "{}_inbox".format(request.method.lower()))
|
||||
|
||||
try:
|
||||
data = handler(request.data, actor=request.actor)
|
||||
|
@ -72,12 +69,10 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
|
|||
return response.Response(status=405)
|
||||
return response.Response({}, status=200)
|
||||
|
||||
@detail_route(methods=['get', 'post'])
|
||||
@detail_route(methods=["get", "post"])
|
||||
def outbox(self, request, *args, **kwargs):
|
||||
system_actor = self.get_object()
|
||||
handler = getattr(system_actor, '{}_outbox'.format(
|
||||
request.method.lower()
|
||||
))
|
||||
handler = getattr(system_actor, "{}_outbox".format(request.method.lower()))
|
||||
try:
|
||||
data = handler(request.data, actor=request.actor)
|
||||
except NotImplementedError:
|
||||
|
@ -90,45 +85,36 @@ class WellKnownViewSet(viewsets.GenericViewSet):
|
|||
permission_classes = []
|
||||
renderer_classes = [renderers.JSONRenderer, renderers.WebfingerRenderer]
|
||||
|
||||
@list_route(methods=['get'])
|
||||
@list_route(methods=["get"])
|
||||
def nodeinfo(self, request, *args, **kwargs):
|
||||
if not preferences.get('instance__nodeinfo_enabled'):
|
||||
if not preferences.get("instance__nodeinfo_enabled"):
|
||||
return HttpResponse(status=404)
|
||||
data = {
|
||||
'links': [
|
||||
"links": [
|
||||
{
|
||||
'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||
'href': utils.full_url(
|
||||
reverse('api:v1:instance:nodeinfo-2.0')
|
||||
)
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": utils.full_url(reverse("api:v1:instance:nodeinfo-2.0")),
|
||||
}
|
||||
]
|
||||
}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(methods=['get'])
|
||||
@list_route(methods=["get"])
|
||||
def webfinger(self, request, *args, **kwargs):
|
||||
if not preferences.get('federation__enabled'):
|
||||
if not preferences.get("federation__enabled"):
|
||||
return HttpResponse(status=405)
|
||||
try:
|
||||
resource_type, resource = webfinger.clean_resource(
|
||||
request.GET['resource'])
|
||||
cleaner = getattr(webfinger, 'clean_{}'.format(resource_type))
|
||||
resource_type, resource = webfinger.clean_resource(request.GET["resource"])
|
||||
cleaner = getattr(webfinger, "clean_{}".format(resource_type))
|
||||
result = cleaner(resource)
|
||||
except forms.ValidationError as e:
|
||||
return response.Response({
|
||||
'errors': {
|
||||
'resource': e.message
|
||||
}
|
||||
}, status=400)
|
||||
return response.Response({"errors": {"resource": e.message}}, status=400)
|
||||
except KeyError:
|
||||
return response.Response({
|
||||
'errors': {
|
||||
'resource': 'This field is required',
|
||||
}
|
||||
}, status=400)
|
||||
return response.Response(
|
||||
{"errors": {"resource": "This field is required"}}, status=400
|
||||
)
|
||||
|
||||
handler = getattr(self, 'handler_{}'.format(resource_type))
|
||||
handler = getattr(self, "handler_{}".format(resource_type))
|
||||
data = handler(result)
|
||||
|
||||
return response.Response(data)
|
||||
|
@ -140,28 +126,25 @@ class WellKnownViewSet(viewsets.GenericViewSet):
|
|||
|
||||
|
||||
class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
|
||||
authentication_classes = [
|
||||
authentication.SignatureAuthentication]
|
||||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
permission_classes = [permissions.LibraryFollower]
|
||||
renderer_classes = [renderers.ActivityPubRenderer]
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
page = request.GET.get('page')
|
||||
library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||
qs = music_models.TrackFile.objects.order_by(
|
||||
'-creation_date'
|
||||
).select_related(
|
||||
'track__artist',
|
||||
'track__album__artist'
|
||||
).filter(library_track__isnull=True)
|
||||
page = request.GET.get("page")
|
||||
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
||||
qs = (
|
||||
music_models.TrackFile.objects.order_by("-creation_date")
|
||||
.select_related("track__artist", "track__album__artist")
|
||||
.filter(library_track__isnull=True)
|
||||
)
|
||||
if page is None:
|
||||
conf = {
|
||||
'id': utils.full_url(reverse('federation:music:files-list')),
|
||||
'page_size': preferences.get(
|
||||
'federation__collection_page_size'),
|
||||
'items': qs,
|
||||
'item_serializer': serializers.AudioSerializer,
|
||||
'actor': library,
|
||||
"id": utils.full_url(reverse("federation:music:files-list")),
|
||||
"page_size": preferences.get("federation__collection_page_size"),
|
||||
"items": qs,
|
||||
"item_serializer": serializers.AudioSerializer,
|
||||
"actor": library,
|
||||
}
|
||||
serializer = serializers.PaginatedCollectionSerializer(conf)
|
||||
data = serializer.data
|
||||
|
@ -169,17 +152,17 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
|
|||
try:
|
||||
page_number = int(page)
|
||||
except:
|
||||
return response.Response(
|
||||
{'page': ['Invalid page number']}, status=400)
|
||||
return response.Response({"page": ["Invalid page number"]}, status=400)
|
||||
p = paginator.Paginator(
|
||||
qs, preferences.get('federation__collection_page_size'))
|
||||
qs, preferences.get("federation__collection_page_size")
|
||||
)
|
||||
try:
|
||||
page = p.page(page_number)
|
||||
conf = {
|
||||
'id': utils.full_url(reverse('federation:music:files-list')),
|
||||
'page': page,
|
||||
'item_serializer': serializers.AudioSerializer,
|
||||
'actor': library,
|
||||
"id": utils.full_url(reverse("federation:music:files-list")),
|
||||
"page": page,
|
||||
"item_serializer": serializers.AudioSerializer,
|
||||
"actor": library,
|
||||
}
|
||||
serializer = serializers.CollectionPageSerializer(conf)
|
||||
data = serializer.data
|
||||
|
@ -190,93 +173,76 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
|
|||
|
||||
|
||||
class LibraryViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ['federation']
|
||||
queryset = models.Library.objects.all().select_related(
|
||||
'actor',
|
||||
'follow',
|
||||
)
|
||||
lookup_field = 'uuid'
|
||||
required_permissions = ["federation"]
|
||||
queryset = models.Library.objects.all().select_related("actor", "follow")
|
||||
lookup_field = "uuid"
|
||||
filter_class = filters.LibraryFilter
|
||||
serializer_class = serializers.APILibrarySerializer
|
||||
ordering_fields = (
|
||||
'id',
|
||||
'creation_date',
|
||||
'fetched_date',
|
||||
'actor__domain',
|
||||
'tracks_count',
|
||||
"id",
|
||||
"creation_date",
|
||||
"fetched_date",
|
||||
"actor__domain",
|
||||
"tracks_count",
|
||||
)
|
||||
|
||||
@list_route(methods=['get'])
|
||||
@list_route(methods=["get"])
|
||||
def fetch(self, request, *args, **kwargs):
|
||||
account = request.GET.get('account')
|
||||
account = request.GET.get("account")
|
||||
if not account:
|
||||
return response.Response(
|
||||
{'account': 'This field is mandatory'}, status=400)
|
||||
return response.Response({"account": "This field is mandatory"}, status=400)
|
||||
|
||||
data = library.scan_from_account_name(account)
|
||||
return response.Response(data)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def scan(self, request, *args, **kwargs):
|
||||
library = self.get_object()
|
||||
serializer = serializers.APILibraryScanSerializer(
|
||||
data=request.data
|
||||
)
|
||||
serializer = serializers.APILibraryScanSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
result = tasks.scan_library.delay(
|
||||
library_id=library.pk,
|
||||
until=serializer.validated_data.get('until')
|
||||
library_id=library.pk, until=serializer.validated_data.get("until")
|
||||
)
|
||||
return response.Response({'task': result.id})
|
||||
return response.Response({"task": result.id})
|
||||
|
||||
@list_route(methods=['get'])
|
||||
@list_route(methods=["get"])
|
||||
def following(self, request, *args, **kwargs):
|
||||
library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||
queryset = models.Follow.objects.filter(
|
||||
actor=library_actor
|
||||
).select_related(
|
||||
'actor',
|
||||
'target',
|
||||
).order_by('-creation_date')
|
||||
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
||||
queryset = (
|
||||
models.Follow.objects.filter(actor=library_actor)
|
||||
.select_related("actor", "target")
|
||||
.order_by("-creation_date")
|
||||
)
|
||||
filterset = filters.FollowFilter(request.GET, queryset=queryset)
|
||||
final_qs = filterset.qs
|
||||
serializer = serializers.APIFollowSerializer(final_qs, many=True)
|
||||
data = {
|
||||
'results': serializer.data,
|
||||
'count': len(final_qs),
|
||||
}
|
||||
data = {"results": serializer.data, "count": len(final_qs)}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(methods=['get', 'patch'])
|
||||
@list_route(methods=["get", "patch"])
|
||||
def followers(self, request, *args, **kwargs):
|
||||
if request.method.lower() == 'patch':
|
||||
serializer = serializers.APILibraryFollowUpdateSerializer(
|
||||
data=request.data)
|
||||
if request.method.lower() == "patch":
|
||||
serializer = serializers.APILibraryFollowUpdateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
follow = serializer.save()
|
||||
return response.Response(
|
||||
serializers.APIFollowSerializer(follow).data
|
||||
)
|
||||
return response.Response(serializers.APIFollowSerializer(follow).data)
|
||||
|
||||
library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||
queryset = models.Follow.objects.filter(
|
||||
target=library_actor
|
||||
).select_related(
|
||||
'actor',
|
||||
'target',
|
||||
).order_by('-creation_date')
|
||||
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
||||
queryset = (
|
||||
models.Follow.objects.filter(target=library_actor)
|
||||
.select_related("actor", "target")
|
||||
.order_by("-creation_date")
|
||||
)
|
||||
filterset = filters.FollowFilter(request.GET, queryset=queryset)
|
||||
final_qs = filterset.qs
|
||||
serializer = serializers.APIFollowSerializer(final_qs, many=True)
|
||||
data = {
|
||||
'results': serializer.data,
|
||||
'count': len(final_qs),
|
||||
}
|
||||
data = {"results": serializer.data, "count": len(final_qs)}
|
||||
return response.Response(data)
|
||||
|
||||
@transaction.atomic
|
||||
|
@ -287,37 +253,32 @@ class LibraryViewSet(
|
|||
return response.Response(serializer.data, status=201)
|
||||
|
||||
|
||||
class LibraryTrackViewSet(
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
class LibraryTrackViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ['federation']
|
||||
queryset = models.LibraryTrack.objects.all().select_related(
|
||||
'library__actor',
|
||||
'library__follow',
|
||||
'local_track_file',
|
||||
).prefetch_related('import_jobs')
|
||||
required_permissions = ["federation"]
|
||||
queryset = (
|
||||
models.LibraryTrack.objects.all()
|
||||
.select_related("library__actor", "library__follow", "local_track_file")
|
||||
.prefetch_related("import_jobs")
|
||||
)
|
||||
filter_class = filters.LibraryTrackFilter
|
||||
serializer_class = serializers.APILibraryTrackSerializer
|
||||
ordering_fields = (
|
||||
'id',
|
||||
'artist_name',
|
||||
'title',
|
||||
'album_title',
|
||||
'creation_date',
|
||||
'modification_date',
|
||||
'fetched_date',
|
||||
'published_date',
|
||||
"id",
|
||||
"artist_name",
|
||||
"title",
|
||||
"album_title",
|
||||
"creation_date",
|
||||
"modification_date",
|
||||
"fetched_date",
|
||||
"published_date",
|
||||
)
|
||||
|
||||
@list_route(methods=['post'])
|
||||
@list_route(methods=["post"])
|
||||
def action(self, request, *args, **kwargs):
|
||||
queryset = models.LibraryTrack.objects.filter(
|
||||
local_track_file__isnull=True)
|
||||
queryset = models.LibraryTrack.objects.filter(local_track_file__isnull=True)
|
||||
serializer = serializers.LibraryTrackActionSerializer(
|
||||
request.data,
|
||||
queryset=queryset,
|
||||
context={'submitted_by': request.user}
|
||||
request.data, queryset=queryset, context={"submitted_by": request.user}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
result = serializer.save()
|
||||
|
|
|
@ -8,36 +8,35 @@ from . import actors
|
|||
from . import utils
|
||||
from . import serializers
|
||||
|
||||
VALID_RESOURCE_TYPES = ['acct']
|
||||
VALID_RESOURCE_TYPES = ["acct"]
|
||||
|
||||
|
||||
def clean_resource(resource_string):
|
||||
if not resource_string:
|
||||
raise forms.ValidationError('Invalid resource string')
|
||||
raise forms.ValidationError("Invalid resource string")
|
||||
|
||||
try:
|
||||
resource_type, resource = resource_string.split(':', 1)
|
||||
resource_type, resource = resource_string.split(":", 1)
|
||||
except ValueError:
|
||||
raise forms.ValidationError('Missing webfinger resource type')
|
||||
raise forms.ValidationError("Missing webfinger resource type")
|
||||
|
||||
if resource_type not in VALID_RESOURCE_TYPES:
|
||||
raise forms.ValidationError('Invalid webfinger resource type')
|
||||
raise forms.ValidationError("Invalid webfinger resource type")
|
||||
|
||||
return resource_type, resource
|
||||
|
||||
|
||||
def clean_acct(acct_string, ensure_local=True):
|
||||
try:
|
||||
username, hostname = acct_string.split('@')
|
||||
username, hostname = acct_string.split("@")
|
||||
except ValueError:
|
||||
raise forms.ValidationError('Invalid format')
|
||||
raise forms.ValidationError("Invalid format")
|
||||
|
||||
if ensure_local and hostname.lower() != settings.FEDERATION_HOSTNAME:
|
||||
raise forms.ValidationError(
|
||||
'Invalid hostname {}'.format(hostname))
|
||||
raise forms.ValidationError("Invalid hostname {}".format(hostname))
|
||||
|
||||
if ensure_local and username not in actors.SYSTEM_ACTORS:
|
||||
raise forms.ValidationError('Invalid username')
|
||||
raise forms.ValidationError("Invalid username")
|
||||
|
||||
return username, hostname
|
||||
|
||||
|
@ -45,12 +44,12 @@ def clean_acct(acct_string, ensure_local=True):
|
|||
def get_resource(resource_string):
|
||||
resource_type, resource = clean_resource(resource_string)
|
||||
username, hostname = clean_acct(resource, ensure_local=False)
|
||||
url = 'https://{}/.well-known/webfinger?resource={}'.format(
|
||||
hostname, resource_string)
|
||||
url = "https://{}/.well-known/webfinger?resource={}".format(
|
||||
hostname, resource_string
|
||||
)
|
||||
response = session.get_session().get(
|
||||
url,
|
||||
verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
|
||||
timeout=5)
|
||||
url, verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, timeout=5
|
||||
)
|
||||
response.raise_for_status()
|
||||
serializer = serializers.ActorWebfingerSerializer(data=response.json())
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
|
|
@ -3,17 +3,14 @@ from funkwhale_api.activity import record
|
|||
|
||||
from . import serializers
|
||||
|
||||
record.registry.register_serializer(
|
||||
serializers.ListeningActivitySerializer)
|
||||
record.registry.register_serializer(serializers.ListeningActivitySerializer)
|
||||
|
||||
|
||||
@record.registry.register_consumer('history.Listening')
|
||||
@record.registry.register_consumer("history.Listening")
|
||||
def broadcast_listening_to_instance_activity(data, obj):
|
||||
if obj.user.privacy_level not in ['instance', 'everyone']:
|
||||
if obj.user.privacy_level not in ["instance", "everyone"]:
|
||||
return
|
||||
|
||||
channels.group_send('instance_activity', {
|
||||
'type': 'event.send',
|
||||
'text': '',
|
||||
'data': data
|
||||
})
|
||||
channels.group_send(
|
||||
"instance_activity", {"type": "event.send", "text": "", "data": data}
|
||||
)
|
||||
|
|
|
@ -2,11 +2,9 @@ from django.contrib import admin
|
|||
|
||||
from . import models
|
||||
|
||||
|
||||
@admin.register(models.Listening)
|
||||
class ListeningAdmin(admin.ModelAdmin):
|
||||
list_display = ['track', 'creation_date', 'user', 'session_key']
|
||||
search_fields = ['track__name', 'user__username']
|
||||
list_select_related = [
|
||||
'user',
|
||||
'track'
|
||||
]
|
||||
list_display = ["track", "creation_date", "user", "session_key"]
|
||||
search_fields = ["track__name", "user__username"]
|
||||
list_select_related = ["user", "track"]
|
||||
|
|
|
@ -11,4 +11,4 @@ class ListeningFactory(factory.django.DjangoModelFactory):
|
|||
track = factory.SubFactory(factories.TrackFactory)
|
||||
|
||||
class Meta:
|
||||
model = 'history.Listening'
|
||||
model = "history.Listening"
|
||||
|
|
|
@ -9,22 +9,52 @@ import django.utils.timezone
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0008_auto_20160529_1456'),
|
||||
("music", "0008_auto_20160529_1456"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Listening',
|
||||
name="Listening",
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
|
||||
('end_date', models.DateTimeField(null=True, blank=True, default=django.utils.timezone.now)),
|
||||
('session_key', models.CharField(null=True, blank=True, max_length=100)),
|
||||
('track', models.ForeignKey(related_name='listenings', to='music.Track', on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(blank=True, null=True, related_name='listenings', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"end_date",
|
||||
models.DateTimeField(
|
||||
null=True, blank=True, default=django.utils.timezone.now
|
||||
),
|
||||
),
|
||||
(
|
||||
"session_key",
|
||||
models.CharField(null=True, blank=True, max_length=100),
|
||||
),
|
||||
(
|
||||
"track",
|
||||
models.ForeignKey(
|
||||
related_name="listenings",
|
||||
to="music.Track",
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="listenings",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-end_date',),
|
||||
},
|
||||
),
|
||||
options={"ordering": ("-end_date",)},
|
||||
)
|
||||
]
|
||||
|
|
|
@ -5,18 +5,13 @@ from django.db import migrations
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('history', '0001_initial'),
|
||||
]
|
||||
dependencies = [("history", "0001_initial")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='listening',
|
||||
options={'ordering': ('-creation_date',)},
|
||||
name="listening", options={"ordering": ("-creation_date",)}
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='listening',
|
||||
old_name='end_date',
|
||||
new_name='creation_date',
|
||||
model_name="listening", old_name="end_date", new_name="creation_date"
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,21 +6,21 @@ from funkwhale_api.music.models import Track
|
|||
|
||||
|
||||
class Listening(models.Model):
|
||||
creation_date = models.DateTimeField(
|
||||
default=timezone.now, null=True, blank=True)
|
||||
creation_date = models.DateTimeField(default=timezone.now, null=True, blank=True)
|
||||
track = models.ForeignKey(
|
||||
Track, related_name="listenings", on_delete=models.CASCADE)
|
||||
Track, related_name="listenings", on_delete=models.CASCADE
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
'users.User',
|
||||
"users.User",
|
||||
related_name="listenings",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE)
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
session_key = models.CharField(max_length=100, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('-creation_date',)
|
||||
ordering = ("-creation_date",)
|
||||
|
||||
def get_activity_url(self):
|
||||
return '{}/listenings/tracks/{}'.format(
|
||||
self.user.get_activity_url(), self.pk)
|
||||
return "{}/listenings/tracks/{}".format(self.user.get_activity_url(), self.pk)
|
||||
|
|
|
@ -9,35 +9,27 @@ from . import models
|
|||
|
||||
class ListeningActivitySerializer(activity_serializers.ModelSerializer):
|
||||
type = serializers.SerializerMethodField()
|
||||
object = TrackActivitySerializer(source='track')
|
||||
actor = UserActivitySerializer(source='user')
|
||||
published = serializers.DateTimeField(source='creation_date')
|
||||
object = TrackActivitySerializer(source="track")
|
||||
actor = UserActivitySerializer(source="user")
|
||||
published = serializers.DateTimeField(source="creation_date")
|
||||
|
||||
class Meta:
|
||||
model = models.Listening
|
||||
fields = [
|
||||
'id',
|
||||
'local_id',
|
||||
'object',
|
||||
'type',
|
||||
'actor',
|
||||
'published'
|
||||
]
|
||||
fields = ["id", "local_id", "object", "type", "actor", "published"]
|
||||
|
||||
def get_actor(self, obj):
|
||||
return UserActivitySerializer(obj.user).data
|
||||
|
||||
def get_type(self, obj):
|
||||
return 'Listen'
|
||||
return "Listen"
|
||||
|
||||
|
||||
class ListeningSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Listening
|
||||
fields = ('id', 'user', 'track', 'creation_date')
|
||||
fields = ("id", "user", "track", "creation_date")
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['user'] = self.context['user']
|
||||
validated_data["user"] = self.context["user"]
|
||||
|
||||
return super().create(validated_data)
|
||||
|
|
|
@ -2,7 +2,8 @@ from django.conf.urls import include, url
|
|||
from . import views
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r'listenings', views.ListeningViewSet, 'listenings')
|
||||
router.register(r"listenings", views.ListeningViewSet, "listenings")
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
|
|
@ -12,9 +12,8 @@ from . import serializers
|
|||
|
||||
|
||||
class ListeningViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
):
|
||||
|
||||
serializer_class = serializers.ListeningSerializer
|
||||
queryset = models.Listening.objects.all()
|
||||
|
@ -31,5 +30,5 @@ class ListeningViewSet(
|
|||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context['user'] = self.request.user
|
||||
context["user"] = self.request.user
|
||||
return context
|
||||
|
|
|
@ -5,4 +5,4 @@ class InstanceActivityConsumer(JsonAuthConsumer):
|
|||
groups = ["instance_activity"]
|
||||
|
||||
def event_send(self, message):
|
||||
self.send_json(message['data'])
|
||||
self.send_json(message["data"])
|
||||
|
|
|
@ -3,91 +3,83 @@ from django.forms import widgets
|
|||
from dynamic_preferences import types
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
|
||||
raven = types.Section('raven')
|
||||
instance = types.Section('instance')
|
||||
raven = types.Section("raven")
|
||||
instance = types.Section("instance")
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceName(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = 'name'
|
||||
default = ''
|
||||
verbose_name = 'Public name'
|
||||
help_text = 'The public name of your instance, displayed in the about page.'
|
||||
field_kwargs = {
|
||||
'required': False,
|
||||
}
|
||||
name = "name"
|
||||
default = ""
|
||||
verbose_name = "Public name"
|
||||
help_text = "The public name of your instance, displayed in the about page."
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceShortDescription(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = 'short_description'
|
||||
default = ''
|
||||
verbose_name = 'Short description'
|
||||
help_text = 'Instance succinct description, displayed in the about page.'
|
||||
field_kwargs = {
|
||||
'required': False,
|
||||
}
|
||||
name = "short_description"
|
||||
default = ""
|
||||
verbose_name = "Short description"
|
||||
help_text = "Instance succinct description, displayed in the about page."
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceLongDescription(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = instance
|
||||
name = 'long_description'
|
||||
verbose_name = 'Long description'
|
||||
default = ''
|
||||
help_text = 'Instance long description, displayed in the about page (markdown allowed).'
|
||||
name = "long_description"
|
||||
verbose_name = "Long description"
|
||||
default = ""
|
||||
help_text = (
|
||||
"Instance long description, displayed in the about page (markdown allowed)."
|
||||
)
|
||||
widget = widgets.Textarea
|
||||
field_kwargs = {
|
||||
'required': False,
|
||||
}
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class RavenDSN(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = raven
|
||||
name = 'front_dsn'
|
||||
default = 'https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4'
|
||||
verbose_name = 'Raven DSN key (front-end)'
|
||||
name = "front_dsn"
|
||||
default = "https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4"
|
||||
verbose_name = "Raven DSN key (front-end)"
|
||||
|
||||
help_text = (
|
||||
'A Raven DSN key used to report front-ent errors to '
|
||||
'a sentry instance. Keeping the default one will report errors to '
|
||||
'Funkwhale developers.'
|
||||
"A Raven DSN key used to report front-ent errors to "
|
||||
"a sentry instance. Keeping the default one will report errors to "
|
||||
"Funkwhale developers."
|
||||
)
|
||||
field_kwargs = {
|
||||
'required': False,
|
||||
}
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class RavenEnabled(types.BooleanPreference):
|
||||
show_in_api = True
|
||||
section = raven
|
||||
name = 'front_enabled'
|
||||
name = "front_enabled"
|
||||
default = False
|
||||
verbose_name = (
|
||||
'Report front-end errors with Raven'
|
||||
)
|
||||
verbose_name = "Report front-end errors with Raven"
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class InstanceNodeinfoEnabled(types.BooleanPreference):
|
||||
show_in_api = False
|
||||
section = instance
|
||||
name = 'nodeinfo_enabled'
|
||||
name = "nodeinfo_enabled"
|
||||
default = True
|
||||
verbose_name = 'Enable nodeinfo endpoint'
|
||||
verbose_name = "Enable nodeinfo endpoint"
|
||||
help_text = (
|
||||
'This endpoint is needed for your about page to work. '
|
||||
'It\'s also helpful for the various monitoring '
|
||||
'tools that map and analyzize the fediverse, '
|
||||
'but you can disable it completely if needed.'
|
||||
"This endpoint is needed for your about page to work. "
|
||||
"It's also helpful for the various monitoring "
|
||||
"tools that map and analyzize the fediverse, "
|
||||
"but you can disable it completely if needed."
|
||||
)
|
||||
|
||||
|
||||
|
@ -95,13 +87,13 @@ class InstanceNodeinfoEnabled(types.BooleanPreference):
|
|||
class InstanceNodeinfoPrivate(types.BooleanPreference):
|
||||
show_in_api = False
|
||||
section = instance
|
||||
name = 'nodeinfo_private'
|
||||
name = "nodeinfo_private"
|
||||
default = False
|
||||
verbose_name = 'Private mode in nodeinfo'
|
||||
verbose_name = "Private mode in nodeinfo"
|
||||
help_text = (
|
||||
'Indicate in the nodeinfo endpoint that you do not want your instance '
|
||||
'to be tracked by third-party services. '
|
||||
'There is no guarantee these tools will honor this setting though.'
|
||||
"Indicate in the nodeinfo endpoint that you do not want your instance "
|
||||
"to be tracked by third-party services. "
|
||||
"There is no guarantee these tools will honor this setting though."
|
||||
)
|
||||
|
||||
|
||||
|
@ -109,10 +101,10 @@ class InstanceNodeinfoPrivate(types.BooleanPreference):
|
|||
class InstanceNodeinfoStatsEnabled(types.BooleanPreference):
|
||||
show_in_api = False
|
||||
section = instance
|
||||
name = 'nodeinfo_stats_enabled'
|
||||
name = "nodeinfo_stats_enabled"
|
||||
default = True
|
||||
verbose_name = 'Enable usage and library stats in nodeinfo endpoint'
|
||||
verbose_name = "Enable usage and library stats in nodeinfo endpoint"
|
||||
help_text = (
|
||||
'Disable this if you don\'t want to share usage and library statistics '
|
||||
'in the nodeinfo endpoint but don\'t want to disable it completely.'
|
||||
"Disable this if you don't want to share usage and library statistics "
|
||||
"in the nodeinfo endpoint but don't want to disable it completely."
|
||||
)
|
||||
|
|
|
@ -6,70 +6,47 @@ from funkwhale_api.common import preferences
|
|||
from . import stats
|
||||
|
||||
|
||||
store = memoize.djangocache.Cache('default')
|
||||
memo = memoize.Memoizer(store, namespace='instance:stats')
|
||||
store = memoize.djangocache.Cache("default")
|
||||
memo = memoize.Memoizer(store, namespace="instance:stats")
|
||||
|
||||
|
||||
def get():
|
||||
share_stats = preferences.get('instance__nodeinfo_stats_enabled')
|
||||
private = preferences.get('instance__nodeinfo_private')
|
||||
share_stats = preferences.get("instance__nodeinfo_stats_enabled")
|
||||
private = preferences.get("instance__nodeinfo_private")
|
||||
data = {
|
||||
'version': '2.0',
|
||||
'software': {
|
||||
'name': 'funkwhale',
|
||||
'version': funkwhale_api.__version__
|
||||
},
|
||||
'protocols': ['activitypub'],
|
||||
'services': {
|
||||
'inbound': [],
|
||||
'outbound': []
|
||||
},
|
||||
'openRegistrations': preferences.get('users__registration_enabled'),
|
||||
'usage': {
|
||||
'users': {
|
||||
'total': 0,
|
||||
}
|
||||
},
|
||||
'metadata': {
|
||||
'private': preferences.get('instance__nodeinfo_private'),
|
||||
'shortDescription': preferences.get('instance__short_description'),
|
||||
'longDescription': preferences.get('instance__long_description'),
|
||||
'nodeName': preferences.get('instance__name'),
|
||||
'library': {
|
||||
'federationEnabled': preferences.get('federation__enabled'),
|
||||
'federationNeedsApproval': preferences.get('federation__music_needs_approval'),
|
||||
'anonymousCanListen': preferences.get('common__api_authentication_required'),
|
||||
"version": "2.0",
|
||||
"software": {"name": "funkwhale", "version": funkwhale_api.__version__},
|
||||
"protocols": ["activitypub"],
|
||||
"services": {"inbound": [], "outbound": []},
|
||||
"openRegistrations": preferences.get("users__registration_enabled"),
|
||||
"usage": {"users": {"total": 0}},
|
||||
"metadata": {
|
||||
"private": preferences.get("instance__nodeinfo_private"),
|
||||
"shortDescription": preferences.get("instance__short_description"),
|
||||
"longDescription": preferences.get("instance__long_description"),
|
||||
"nodeName": preferences.get("instance__name"),
|
||||
"library": {
|
||||
"federationEnabled": preferences.get("federation__enabled"),
|
||||
"federationNeedsApproval": preferences.get(
|
||||
"federation__music_needs_approval"
|
||||
),
|
||||
"anonymousCanListen": preferences.get(
|
||||
"common__api_authentication_required"
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
if share_stats:
|
||||
getter = memo(
|
||||
lambda: stats.get(),
|
||||
max_age=600
|
||||
)
|
||||
getter = memo(lambda: stats.get(), max_age=600)
|
||||
statistics = getter()
|
||||
data['usage']['users']['total'] = statistics['users']
|
||||
data['metadata']['library']['tracks'] = {
|
||||
'total': statistics['tracks'],
|
||||
}
|
||||
data['metadata']['library']['artists'] = {
|
||||
'total': statistics['artists'],
|
||||
}
|
||||
data['metadata']['library']['albums'] = {
|
||||
'total': statistics['albums'],
|
||||
}
|
||||
data['metadata']['library']['music'] = {
|
||||
'hours': statistics['music_duration']
|
||||
}
|
||||
data["usage"]["users"]["total"] = statistics["users"]
|
||||
data["metadata"]["library"]["tracks"] = {"total": statistics["tracks"]}
|
||||
data["metadata"]["library"]["artists"] = {"total": statistics["artists"]}
|
||||
data["metadata"]["library"]["albums"] = {"total": statistics["albums"]}
|
||||
data["metadata"]["library"]["music"] = {"hours": statistics["music_duration"]}
|
||||
|
||||
data['metadata']['usage'] = {
|
||||
'favorites': {
|
||||
'tracks': {
|
||||
'total': statistics['track_favorites'],
|
||||
}
|
||||
},
|
||||
'listenings': {
|
||||
'total': statistics['listenings']
|
||||
}
|
||||
data["metadata"]["usage"] = {
|
||||
"favorites": {"tracks": {"total": statistics["track_favorites"]}},
|
||||
"listenings": {"total": statistics["listenings"]},
|
||||
}
|
||||
return data
|
||||
|
|
|
@ -8,13 +8,13 @@ from funkwhale_api.users.models import User
|
|||
|
||||
def get():
|
||||
return {
|
||||
'users': get_users(),
|
||||
'tracks': get_tracks(),
|
||||
'albums': get_albums(),
|
||||
'artists': get_artists(),
|
||||
'track_favorites': get_track_favorites(),
|
||||
'listenings': get_listenings(),
|
||||
'music_duration': get_music_duration(),
|
||||
"users": get_users(),
|
||||
"tracks": get_tracks(),
|
||||
"albums": get_albums(),
|
||||
"artists": get_artists(),
|
||||
"track_favorites": get_track_favorites(),
|
||||
"listenings": get_listenings(),
|
||||
"music_duration": get_music_duration(),
|
||||
}
|
||||
|
||||
|
||||
|
@ -43,9 +43,7 @@ def get_artists():
|
|||
|
||||
|
||||
def get_music_duration():
|
||||
seconds = models.TrackFile.objects.aggregate(
|
||||
d=Sum('duration'),
|
||||
)['d']
|
||||
seconds = models.TrackFile.objects.aggregate(d=Sum("duration"))["d"]
|
||||
if seconds:
|
||||
return seconds / 3600
|
||||
return 0
|
||||
|
|
|
@ -2,10 +2,11 @@ from django.conf.urls import url
|
|||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
|
||||
admin_router = routers.SimpleRouter()
|
||||
admin_router.register(r'admin/settings', views.AdminSettings, 'admin-settings')
|
||||
admin_router.register(r"admin/settings", views.AdminSettings, "admin-settings")
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^nodeinfo/2.0/$', views.NodeInfo.as_view(), name='nodeinfo-2.0'),
|
||||
url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'),
|
||||
url(r"^nodeinfo/2.0/$", views.NodeInfo.as_view(), name="nodeinfo-2.0"),
|
||||
url(r"^settings/$", views.InstanceSettings.as_view(), name="settings"),
|
||||
] + admin_router.urls
|
||||
|
|
|
@ -12,15 +12,14 @@ from . import nodeinfo
|
|||
from . import stats
|
||||
|
||||
|
||||
NODEINFO_2_CONTENT_TYPE = (
|
||||
'application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8' # noqa
|
||||
)
|
||||
NODEINFO_2_CONTENT_TYPE = "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8" # noqa
|
||||
|
||||
|
||||
class AdminSettings(preferences_viewsets.GlobalPreferencesViewSet):
|
||||
pagination_class = None
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ['settings']
|
||||
required_permissions = ["settings"]
|
||||
|
||||
|
||||
class InstanceSettings(views.APIView):
|
||||
permission_classes = []
|
||||
|
@ -29,16 +28,11 @@ class InstanceSettings(views.APIView):
|
|||
def get(self, request, *args, **kwargs):
|
||||
manager = global_preferences_registry.manager()
|
||||
manager.all()
|
||||
all_preferences = manager.model.objects.all().order_by(
|
||||
'section', 'name'
|
||||
)
|
||||
all_preferences = manager.model.objects.all().order_by("section", "name")
|
||||
api_preferences = [
|
||||
p
|
||||
for p in all_preferences
|
||||
if getattr(p.preference, 'show_in_api', False)
|
||||
p for p in all_preferences if getattr(p.preference, "show_in_api", False)
|
||||
]
|
||||
data = serializers.GlobalPreferenceSerializer(
|
||||
api_preferences, many=True).data
|
||||
data = serializers.GlobalPreferenceSerializer(api_preferences, many=True).data
|
||||
return Response(data, status=200)
|
||||
|
||||
|
||||
|
@ -47,8 +41,7 @@ class NodeInfo(views.APIView):
|
|||
authentication_classes = []
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not preferences.get('instance__nodeinfo_enabled'):
|
||||
if not preferences.get("instance__nodeinfo_enabled"):
|
||||
return Response(status=404)
|
||||
data = nodeinfo.get()
|
||||
return Response(
|
||||
data, status=200, content_type=NODEINFO_2_CONTENT_TYPE)
|
||||
return Response(data, status=200, content_type=NODEINFO_2_CONTENT_TYPE)
|
||||
|
|
|
@ -7,19 +7,15 @@ from funkwhale_api.music import models as music_models
|
|||
|
||||
|
||||
class ManageTrackFileFilterSet(filters.FilterSet):
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'track__title',
|
||||
'track__album__title',
|
||||
'track__artist__name',
|
||||
'source',
|
||||
])
|
||||
q = fields.SearchFilter(
|
||||
search_fields=[
|
||||
"track__title",
|
||||
"track__album__title",
|
||||
"track__artist__name",
|
||||
"source",
|
||||
]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = music_models.TrackFile
|
||||
fields = [
|
||||
'q',
|
||||
'track__album',
|
||||
'track__artist',
|
||||
'track',
|
||||
'library_track'
|
||||
]
|
||||
fields = ["q", "track__album", "track__artist", "track", "library_track"]
|
||||
|
|
|
@ -10,12 +10,7 @@ from . import filters
|
|||
class ManageTrackFileArtistSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = music_models.Artist
|
||||
fields = [
|
||||
'id',
|
||||
'mbid',
|
||||
'creation_date',
|
||||
'name',
|
||||
]
|
||||
fields = ["id", "mbid", "creation_date", "name"]
|
||||
|
||||
|
||||
class ManageTrackFileAlbumSerializer(serializers.ModelSerializer):
|
||||
|
@ -24,13 +19,13 @@ class ManageTrackFileAlbumSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = music_models.Album
|
||||
fields = (
|
||||
'id',
|
||||
'mbid',
|
||||
'title',
|
||||
'artist',
|
||||
'release_date',
|
||||
'cover',
|
||||
'creation_date',
|
||||
"id",
|
||||
"mbid",
|
||||
"title",
|
||||
"artist",
|
||||
"release_date",
|
||||
"cover",
|
||||
"creation_date",
|
||||
)
|
||||
|
||||
|
||||
|
@ -40,15 +35,7 @@ class ManageTrackFileTrackSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = music_models.Track
|
||||
fields = (
|
||||
'id',
|
||||
'mbid',
|
||||
'title',
|
||||
'album',
|
||||
'artist',
|
||||
'creation_date',
|
||||
'position',
|
||||
)
|
||||
fields = ("id", "mbid", "title", "album", "artist", "creation_date", "position")
|
||||
|
||||
|
||||
class ManageTrackFileSerializer(serializers.ModelSerializer):
|
||||
|
@ -57,24 +44,24 @@ class ManageTrackFileSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = music_models.TrackFile
|
||||
fields = (
|
||||
'id',
|
||||
'path',
|
||||
'source',
|
||||
'filename',
|
||||
'mimetype',
|
||||
'track',
|
||||
'duration',
|
||||
'mimetype',
|
||||
'bitrate',
|
||||
'size',
|
||||
'path',
|
||||
'library_track',
|
||||
"id",
|
||||
"path",
|
||||
"source",
|
||||
"filename",
|
||||
"mimetype",
|
||||
"track",
|
||||
"duration",
|
||||
"mimetype",
|
||||
"bitrate",
|
||||
"size",
|
||||
"path",
|
||||
"library_track",
|
||||
)
|
||||
|
||||
|
||||
class ManageTrackFileActionSerializer(common_serializers.ActionSerializer):
|
||||
actions = ['delete']
|
||||
dangerous_actions = ['delete']
|
||||
actions = ["delete"]
|
||||
dangerous_actions = ["delete"]
|
||||
filterset_class = filters.ManageTrackFileFilterSet
|
||||
|
||||
@transaction.atomic
|
||||
|
|
|
@ -2,10 +2,10 @@ from django.conf.urls import include, url
|
|||
from . import views
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
library_router = routers.SimpleRouter()
|
||||
library_router.register(r'track-files', views.ManageTrackFileViewSet, 'track-files')
|
||||
library_router.register(r"track-files", views.ManageTrackFileViewSet, "track-files")
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^library/',
|
||||
include((library_router.urls, 'instance'), namespace='library')),
|
||||
url(r"^library/", include((library_router.urls, "instance"), namespace="library"))
|
||||
]
|
||||
|
|
|
@ -11,38 +11,35 @@ from . import serializers
|
|||
|
||||
|
||||
class ManageTrackFileViewSet(
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
queryset = (
|
||||
music_models.TrackFile.objects.all()
|
||||
.select_related(
|
||||
'track__artist',
|
||||
'track__album__artist',
|
||||
'library_track')
|
||||
.order_by('-id')
|
||||
.select_related("track__artist", "track__album__artist", "library_track")
|
||||
.order_by("-id")
|
||||
)
|
||||
serializer_class = serializers.ManageTrackFileSerializer
|
||||
filter_class = filters.ManageTrackFileFilterSet
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ['library']
|
||||
required_permissions = ["library"]
|
||||
ordering_fields = [
|
||||
'accessed_date',
|
||||
'modification_date',
|
||||
'creation_date',
|
||||
'track__artist__name',
|
||||
'bitrate',
|
||||
'size',
|
||||
'duration',
|
||||
"accessed_date",
|
||||
"modification_date",
|
||||
"creation_date",
|
||||
"track__artist__name",
|
||||
"bitrate",
|
||||
"size",
|
||||
"duration",
|
||||
]
|
||||
|
||||
@list_route(methods=['post'])
|
||||
@list_route(methods=["post"])
|
||||
def action(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = serializers.ManageTrackFileActionSerializer(
|
||||
request.data,
|
||||
queryset=queryset,
|
||||
request.data, queryset=queryset
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
result = serializer.save()
|
||||
|
|
|
@ -5,85 +5,73 @@ from . import models
|
|||
|
||||
@admin.register(models.Artist)
|
||||
class ArtistAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'mbid', 'creation_date']
|
||||
search_fields = ['name', 'mbid']
|
||||
list_display = ["name", "mbid", "creation_date"]
|
||||
search_fields = ["name", "mbid"]
|
||||
|
||||
|
||||
@admin.register(models.Album)
|
||||
class AlbumAdmin(admin.ModelAdmin):
|
||||
list_display = ['title', 'artist', 'mbid', 'release_date', 'creation_date']
|
||||
search_fields = ['title', 'artist__name', 'mbid']
|
||||
list_display = ["title", "artist", "mbid", "release_date", "creation_date"]
|
||||
search_fields = ["title", "artist__name", "mbid"]
|
||||
list_select_related = True
|
||||
|
||||
|
||||
@admin.register(models.Track)
|
||||
class TrackAdmin(admin.ModelAdmin):
|
||||
list_display = ['title', 'artist', 'album', 'mbid']
|
||||
search_fields = ['title', 'artist__name', 'album__title', 'mbid']
|
||||
list_display = ["title", "artist", "album", "mbid"]
|
||||
search_fields = ["title", "artist__name", "album__title", "mbid"]
|
||||
list_select_related = True
|
||||
|
||||
|
||||
@admin.register(models.ImportBatch)
|
||||
class ImportBatchAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'submitted_by',
|
||||
'creation_date',
|
||||
'import_request',
|
||||
'status']
|
||||
list_select_related = [
|
||||
'submitted_by',
|
||||
'import_request',
|
||||
]
|
||||
list_filter = ['status']
|
||||
search_fields = [
|
||||
'import_request__name', 'source', 'batch__pk', 'mbid']
|
||||
list_display = ["submitted_by", "creation_date", "import_request", "status"]
|
||||
list_select_related = ["submitted_by", "import_request"]
|
||||
list_filter = ["status"]
|
||||
search_fields = ["import_request__name", "source", "batch__pk", "mbid"]
|
||||
|
||||
|
||||
@admin.register(models.ImportJob)
|
||||
class ImportJobAdmin(admin.ModelAdmin):
|
||||
list_display = ['source', 'batch', 'track_file', 'status', 'mbid']
|
||||
list_select_related = [
|
||||
'track_file',
|
||||
'batch',
|
||||
]
|
||||
search_fields = ['source', 'batch__pk', 'mbid']
|
||||
list_filter = ['status']
|
||||
list_display = ["source", "batch", "track_file", "status", "mbid"]
|
||||
list_select_related = ["track_file", "batch"]
|
||||
search_fields = ["source", "batch__pk", "mbid"]
|
||||
list_filter = ["status"]
|
||||
|
||||
|
||||
@admin.register(models.Work)
|
||||
class WorkAdmin(admin.ModelAdmin):
|
||||
list_display = ['title', 'mbid', 'language', 'nature']
|
||||
list_display = ["title", "mbid", "language", "nature"]
|
||||
list_select_related = True
|
||||
search_fields = ['title']
|
||||
list_filter = ['language', 'nature']
|
||||
search_fields = ["title"]
|
||||
list_filter = ["language", "nature"]
|
||||
|
||||
|
||||
@admin.register(models.Lyrics)
|
||||
class LyricsAdmin(admin.ModelAdmin):
|
||||
list_display = ['url', 'id', 'url']
|
||||
list_display = ["url", "id", "url"]
|
||||
list_select_related = True
|
||||
search_fields = ['url', 'work__title']
|
||||
list_filter = ['work__language']
|
||||
search_fields = ["url", "work__title"]
|
||||
list_filter = ["work__language"]
|
||||
|
||||
|
||||
@admin.register(models.TrackFile)
|
||||
class TrackFileAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'track',
|
||||
'audio_file',
|
||||
'source',
|
||||
'duration',
|
||||
'mimetype',
|
||||
'size',
|
||||
'bitrate'
|
||||
]
|
||||
list_select_related = [
|
||||
'track'
|
||||
"track",
|
||||
"audio_file",
|
||||
"source",
|
||||
"duration",
|
||||
"mimetype",
|
||||
"size",
|
||||
"bitrate",
|
||||
]
|
||||
list_select_related = ["track"]
|
||||
search_fields = [
|
||||
'source',
|
||||
'acoustid_track_id',
|
||||
'track__title',
|
||||
'track__album__title',
|
||||
'track__artist__name']
|
||||
list_filter = ['mimetype']
|
||||
"source",
|
||||
"acoustid_track_id",
|
||||
"track__title",
|
||||
"track__album__title",
|
||||
"track__artist__name",
|
||||
]
|
||||
list_filter = ["mimetype"]
|
||||
|
|
|
@ -2,78 +2,72 @@ import factory
|
|||
import os
|
||||
|
||||
from funkwhale_api.factories import registry, ManyToManyFromList
|
||||
from funkwhale_api.federation.factories import (
|
||||
LibraryTrackFactory,
|
||||
)
|
||||
from funkwhale_api.federation.factories import LibraryTrackFactory
|
||||
from funkwhale_api.users.factories import UserFactory
|
||||
|
||||
SAMPLES_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||
'tests', 'music'
|
||||
"tests",
|
||||
"music",
|
||||
)
|
||||
|
||||
|
||||
@registry.register
|
||||
class ArtistFactory(factory.django.DjangoModelFactory):
|
||||
name = factory.Faker('name')
|
||||
mbid = factory.Faker('uuid4')
|
||||
name = factory.Faker("name")
|
||||
mbid = factory.Faker("uuid4")
|
||||
|
||||
class Meta:
|
||||
model = 'music.Artist'
|
||||
model = "music.Artist"
|
||||
|
||||
|
||||
@registry.register
|
||||
class AlbumFactory(factory.django.DjangoModelFactory):
|
||||
title = factory.Faker('sentence', nb_words=3)
|
||||
mbid = factory.Faker('uuid4')
|
||||
release_date = factory.Faker('date_object')
|
||||
title = factory.Faker("sentence", nb_words=3)
|
||||
mbid = factory.Faker("uuid4")
|
||||
release_date = factory.Faker("date_object")
|
||||
cover = factory.django.ImageField()
|
||||
artist = factory.SubFactory(ArtistFactory)
|
||||
release_group_id = factory.Faker('uuid4')
|
||||
release_group_id = factory.Faker("uuid4")
|
||||
|
||||
class Meta:
|
||||
model = 'music.Album'
|
||||
model = "music.Album"
|
||||
|
||||
|
||||
@registry.register
|
||||
class TrackFactory(factory.django.DjangoModelFactory):
|
||||
title = factory.Faker('sentence', nb_words=3)
|
||||
mbid = factory.Faker('uuid4')
|
||||
title = factory.Faker("sentence", nb_words=3)
|
||||
mbid = factory.Faker("uuid4")
|
||||
album = factory.SubFactory(AlbumFactory)
|
||||
artist = factory.SelfAttribute('album.artist')
|
||||
artist = factory.SelfAttribute("album.artist")
|
||||
position = 1
|
||||
tags = ManyToManyFromList('tags')
|
||||
tags = ManyToManyFromList("tags")
|
||||
|
||||
class Meta:
|
||||
model = 'music.Track'
|
||||
model = "music.Track"
|
||||
|
||||
|
||||
@registry.register
|
||||
class TrackFileFactory(factory.django.DjangoModelFactory):
|
||||
track = factory.SubFactory(TrackFactory)
|
||||
audio_file = factory.django.FileField(
|
||||
from_path=os.path.join(SAMPLES_PATH, 'test.ogg'))
|
||||
from_path=os.path.join(SAMPLES_PATH, "test.ogg")
|
||||
)
|
||||
|
||||
bitrate = None
|
||||
size = None
|
||||
duration = None
|
||||
|
||||
class Meta:
|
||||
model = 'music.TrackFile'
|
||||
model = "music.TrackFile"
|
||||
|
||||
class Params:
|
||||
in_place = factory.Trait(
|
||||
audio_file=None,
|
||||
)
|
||||
in_place = factory.Trait(audio_file=None)
|
||||
federation = factory.Trait(
|
||||
audio_file=None,
|
||||
library_track=factory.SubFactory(LibraryTrackFactory),
|
||||
mimetype=factory.LazyAttribute(
|
||||
lambda o: o.library_track.audio_mimetype
|
||||
),
|
||||
source=factory.LazyAttribute(
|
||||
lambda o: o.library_track.audio_url
|
||||
),
|
||||
mimetype=factory.LazyAttribute(lambda o: o.library_track.audio_mimetype),
|
||||
source=factory.LazyAttribute(lambda o: o.library_track.audio_url),
|
||||
)
|
||||
|
||||
|
||||
|
@ -82,26 +76,21 @@ class ImportBatchFactory(factory.django.DjangoModelFactory):
|
|||
submitted_by = factory.SubFactory(UserFactory)
|
||||
|
||||
class Meta:
|
||||
model = 'music.ImportBatch'
|
||||
model = "music.ImportBatch"
|
||||
|
||||
class Params:
|
||||
federation = factory.Trait(
|
||||
submitted_by=None,
|
||||
source='federation',
|
||||
)
|
||||
finished = factory.Trait(
|
||||
status='finished',
|
||||
)
|
||||
federation = factory.Trait(submitted_by=None, source="federation")
|
||||
finished = factory.Trait(status="finished")
|
||||
|
||||
|
||||
@registry.register
|
||||
class ImportJobFactory(factory.django.DjangoModelFactory):
|
||||
batch = factory.SubFactory(ImportBatchFactory)
|
||||
source = factory.Faker('url')
|
||||
mbid = factory.Faker('uuid4')
|
||||
source = factory.Faker("url")
|
||||
mbid = factory.Faker("uuid4")
|
||||
|
||||
class Meta:
|
||||
model = 'music.ImportJob'
|
||||
model = "music.ImportJob"
|
||||
|
||||
class Params:
|
||||
federation = factory.Trait(
|
||||
|
@ -110,53 +99,51 @@ class ImportJobFactory(factory.django.DjangoModelFactory):
|
|||
batch=factory.SubFactory(ImportBatchFactory, federation=True),
|
||||
)
|
||||
finished = factory.Trait(
|
||||
status='finished',
|
||||
track_file=factory.SubFactory(TrackFileFactory),
|
||||
)
|
||||
in_place = factory.Trait(
|
||||
status='finished',
|
||||
audio_file=None,
|
||||
status="finished", track_file=factory.SubFactory(TrackFileFactory)
|
||||
)
|
||||
in_place = factory.Trait(status="finished", audio_file=None)
|
||||
with_audio_file = factory.Trait(
|
||||
status='finished',
|
||||
status="finished",
|
||||
audio_file=factory.django.FileField(
|
||||
from_path=os.path.join(SAMPLES_PATH, 'test.ogg')),
|
||||
from_path=os.path.join(SAMPLES_PATH, "test.ogg")
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@registry.register(name='music.FileImportJob')
|
||||
@registry.register(name="music.FileImportJob")
|
||||
class FileImportJobFactory(ImportJobFactory):
|
||||
source = 'file://'
|
||||
source = "file://"
|
||||
mbid = None
|
||||
audio_file = factory.django.FileField(
|
||||
from_path=os.path.join(SAMPLES_PATH, 'test.ogg'))
|
||||
from_path=os.path.join(SAMPLES_PATH, "test.ogg")
|
||||
)
|
||||
|
||||
|
||||
@registry.register
|
||||
class WorkFactory(factory.django.DjangoModelFactory):
|
||||
mbid = factory.Faker('uuid4')
|
||||
language = 'eng'
|
||||
nature = 'song'
|
||||
title = factory.Faker('sentence', nb_words=3)
|
||||
mbid = factory.Faker("uuid4")
|
||||
language = "eng"
|
||||
nature = "song"
|
||||
title = factory.Faker("sentence", nb_words=3)
|
||||
|
||||
class Meta:
|
||||
model = 'music.Work'
|
||||
model = "music.Work"
|
||||
|
||||
|
||||
@registry.register
|
||||
class LyricsFactory(factory.django.DjangoModelFactory):
|
||||
work = factory.SubFactory(WorkFactory)
|
||||
url = factory.Faker('url')
|
||||
content = factory.Faker('paragraphs', nb=4)
|
||||
url = factory.Faker("url")
|
||||
content = factory.Faker("paragraphs", nb=4)
|
||||
|
||||
class Meta:
|
||||
model = 'music.Lyrics'
|
||||
model = "music.Lyrics"
|
||||
|
||||
|
||||
@registry.register
|
||||
class TagFactory(factory.django.DjangoModelFactory):
|
||||
name = factory.SelfAttribute('slug')
|
||||
slug = factory.Faker('slug')
|
||||
name = factory.SelfAttribute("slug")
|
||||
slug = factory.Faker("slug")
|
||||
|
||||
class Meta:
|
||||
model = 'taggit.Tag'
|
||||
model = "taggit.Tag"
|
||||
|
|
|
@ -10,13 +10,15 @@ from funkwhale_api.music import factories
|
|||
def create_data(count=25):
|
||||
artists = factories.ArtistFactory.create_batch(size=count)
|
||||
for artist in artists:
|
||||
print('Creating data for', artist)
|
||||
print("Creating data for", artist)
|
||||
albums = factories.AlbumFactory.create_batch(
|
||||
artist=artist, size=random.randint(1, 5))
|
||||
artist=artist, size=random.randint(1, 5)
|
||||
)
|
||||
for album in albums:
|
||||
factories.TrackFileFactory.create_batch(
|
||||
track__album=album, size=random.randint(3, 18))
|
||||
track__album=album, size=random.randint(3, 18)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
create_data()
|
||||
|
|
|
@ -7,12 +7,10 @@ from . import models
|
|||
|
||||
|
||||
class ListenableMixin(filters.FilterSet):
|
||||
listenable = filters.BooleanFilter(name='_', method='filter_listenable')
|
||||
listenable = filters.BooleanFilter(name="_", method="filter_listenable")
|
||||
|
||||
def filter_listenable(self, queryset, name, value):
|
||||
queryset = queryset.annotate(
|
||||
files_count=Count('tracks__files')
|
||||
)
|
||||
queryset = queryset.annotate(files_count=Count("tracks__files"))
|
||||
if value:
|
||||
return queryset.filter(files_count__gt=0)
|
||||
else:
|
||||
|
@ -20,39 +18,31 @@ class ListenableMixin(filters.FilterSet):
|
|||
|
||||
|
||||
class ArtistFilter(ListenableMixin):
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'name',
|
||||
])
|
||||
q = fields.SearchFilter(search_fields=["name"])
|
||||
|
||||
class Meta:
|
||||
model = models.Artist
|
||||
fields = {
|
||||
'name': ['exact', 'iexact', 'startswith', 'icontains'],
|
||||
'listenable': 'exact',
|
||||
"name": ["exact", "iexact", "startswith", "icontains"],
|
||||
"listenable": "exact",
|
||||
}
|
||||
|
||||
|
||||
class TrackFilter(filters.FilterSet):
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'title',
|
||||
'album__title',
|
||||
'artist__name',
|
||||
])
|
||||
listenable = filters.BooleanFilter(name='_', method='filter_listenable')
|
||||
q = fields.SearchFilter(search_fields=["title", "album__title", "artist__name"])
|
||||
listenable = filters.BooleanFilter(name="_", method="filter_listenable")
|
||||
|
||||
class Meta:
|
||||
model = models.Track
|
||||
fields = {
|
||||
'title': ['exact', 'iexact', 'startswith', 'icontains'],
|
||||
'listenable': ['exact'],
|
||||
'artist': ['exact'],
|
||||
'album': ['exact'],
|
||||
"title": ["exact", "iexact", "startswith", "icontains"],
|
||||
"listenable": ["exact"],
|
||||
"artist": ["exact"],
|
||||
"album": ["exact"],
|
||||
}
|
||||
|
||||
def filter_listenable(self, queryset, name, value):
|
||||
queryset = queryset.annotate(
|
||||
files_count=Count('files')
|
||||
)
|
||||
queryset = queryset.annotate(files_count=Count("files"))
|
||||
if value:
|
||||
return queryset.filter(files_count__gt=0)
|
||||
else:
|
||||
|
@ -60,46 +50,32 @@ class TrackFilter(filters.FilterSet):
|
|||
|
||||
|
||||
class ImportBatchFilter(filters.FilterSet):
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'submitted_by__username',
|
||||
'source',
|
||||
])
|
||||
q = fields.SearchFilter(search_fields=["submitted_by__username", "source"])
|
||||
|
||||
class Meta:
|
||||
model = models.ImportBatch
|
||||
fields = {
|
||||
'status': ['exact'],
|
||||
'source': ['exact'],
|
||||
'submitted_by': ['exact'],
|
||||
}
|
||||
fields = {"status": ["exact"], "source": ["exact"], "submitted_by": ["exact"]}
|
||||
|
||||
|
||||
class ImportJobFilter(filters.FilterSet):
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'batch__submitted_by__username',
|
||||
'source',
|
||||
])
|
||||
q = fields.SearchFilter(search_fields=["batch__submitted_by__username", "source"])
|
||||
|
||||
class Meta:
|
||||
model = models.ImportJob
|
||||
fields = {
|
||||
'batch': ['exact'],
|
||||
'batch__status': ['exact'],
|
||||
'batch__source': ['exact'],
|
||||
'batch__submitted_by': ['exact'],
|
||||
'status': ['exact'],
|
||||
'source': ['exact'],
|
||||
"batch": ["exact"],
|
||||
"batch__status": ["exact"],
|
||||
"batch__source": ["exact"],
|
||||
"batch__submitted_by": ["exact"],
|
||||
"status": ["exact"],
|
||||
"source": ["exact"],
|
||||
}
|
||||
|
||||
|
||||
class AlbumFilter(ListenableMixin):
|
||||
listenable = filters.BooleanFilter(name='_', method='filter_listenable')
|
||||
q = fields.SearchFilter(search_fields=[
|
||||
'title',
|
||||
'artist__name'
|
||||
'source',
|
||||
])
|
||||
listenable = filters.BooleanFilter(name="_", method="filter_listenable")
|
||||
q = fields.SearchFilter(search_fields=["title", "artist__name" "source"])
|
||||
|
||||
class Meta:
|
||||
model = models.Album
|
||||
fields = ['listenable', 'q', 'artist']
|
||||
fields = ["listenable", "q", "artist"]
|
||||
|
|
|
@ -1,42 +1,43 @@
|
|||
|
||||
|
||||
def load(model, *args, **kwargs):
|
||||
importer = registry[model.__name__](model=model)
|
||||
return importer.load(*args, **kwargs)
|
||||
|
||||
|
||||
class Importer(object):
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
|
||||
def load(self, cleaned_data, raw_data, import_hooks):
|
||||
mbid = cleaned_data.pop('mbid')
|
||||
mbid = cleaned_data.pop("mbid")
|
||||
m = self.model.objects.update_or_create(mbid=mbid, defaults=cleaned_data)[0]
|
||||
for hook in import_hooks:
|
||||
hook(m, cleaned_data, raw_data)
|
||||
return m
|
||||
|
||||
|
||||
class Mapping(object):
|
||||
"""Cast musicbrainz data to funkwhale data and vice-versa"""
|
||||
|
||||
def __init__(self, musicbrainz_mapping):
|
||||
self.musicbrainz_mapping = musicbrainz_mapping
|
||||
|
||||
self._from_musicbrainz = {}
|
||||
self._to_musicbrainz = {}
|
||||
for field_name, conf in self.musicbrainz_mapping.items():
|
||||
self._from_musicbrainz[conf['musicbrainz_field_name']] = {
|
||||
'field_name': field_name,
|
||||
'converter': conf.get('converter', lambda v: v)
|
||||
self._from_musicbrainz[conf["musicbrainz_field_name"]] = {
|
||||
"field_name": field_name,
|
||||
"converter": conf.get("converter", lambda v: v),
|
||||
}
|
||||
self._to_musicbrainz[field_name] = {
|
||||
'field_name': conf['musicbrainz_field_name'],
|
||||
'converter': conf.get('converter', lambda v: v)
|
||||
"field_name": conf["musicbrainz_field_name"],
|
||||
"converter": conf.get("converter", lambda v: v),
|
||||
}
|
||||
def from_musicbrainz(self, key, value):
|
||||
return self._from_musicbrainz[key]['field_name'], self._from_musicbrainz[key]['converter'](value)
|
||||
|
||||
registry = {
|
||||
'Artist': Importer,
|
||||
'Track': Importer,
|
||||
'Album': Importer,
|
||||
'Work': Importer,
|
||||
}
|
||||
def from_musicbrainz(self, key, value):
|
||||
return (
|
||||
self._from_musicbrainz[key]["field_name"],
|
||||
self._from_musicbrainz[key]["converter"](value),
|
||||
)
|
||||
|
||||
|
||||
registry = {"Artist": Importer, "Track": Importer, "Album": Importer, "Work": Importer}
|
||||
|
|
|
@ -6,22 +6,22 @@ from bs4 import BeautifulSoup
|
|||
def _get_html(url):
|
||||
with urllib.request.urlopen(url) as response:
|
||||
html = response.read()
|
||||
return html.decode('utf-8')
|
||||
return html.decode("utf-8")
|
||||
|
||||
|
||||
def extract_content(html):
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
return soup.find_all("div", class_='lyricbox')[0].contents
|
||||
return soup.find_all("div", class_="lyricbox")[0].contents
|
||||
|
||||
|
||||
def clean_content(contents):
|
||||
final_content = ""
|
||||
for e in contents:
|
||||
if e == '\n':
|
||||
if e == "\n":
|
||||
continue
|
||||
if e.name == 'script':
|
||||
if e.name == "script":
|
||||
continue
|
||||
if e.name == 'br':
|
||||
if e.name == "br":
|
||||
final_content += "\n"
|
||||
continue
|
||||
try:
|
||||
|
|
|
@ -10,20 +10,20 @@ from funkwhale_api.music import models, utils
|
|||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Run common checks and fix against imported tracks'
|
||||
help = "Run common checks and fix against imported tracks"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
dest='dry_run',
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
dest="dry_run",
|
||||
default=False,
|
||||
help='Do not execute anything'
|
||||
help="Do not execute anything",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['dry_run']:
|
||||
self.stdout.write('Dry-run on, will not commit anything')
|
||||
if options["dry_run"]:
|
||||
self.stdout.write("Dry-run on, will not commit anything")
|
||||
self.fix_mimetypes(**options)
|
||||
self.fix_file_data(**options)
|
||||
self.fix_file_size(**options)
|
||||
|
@ -31,75 +31,73 @@ class Command(BaseCommand):
|
|||
|
||||
@transaction.atomic
|
||||
def fix_mimetypes(self, dry_run, **kwargs):
|
||||
self.stdout.write('Fixing missing mimetypes...')
|
||||
self.stdout.write("Fixing missing mimetypes...")
|
||||
matching = models.TrackFile.objects.filter(
|
||||
source__startswith='file://').exclude(mimetype__startswith='audio/')
|
||||
source__startswith="file://"
|
||||
).exclude(mimetype__startswith="audio/")
|
||||
self.stdout.write(
|
||||
'[mimetypes] {} entries found with bad or no mimetype'.format(
|
||||
matching.count()))
|
||||
"[mimetypes] {} entries found with bad or no mimetype".format(
|
||||
matching.count()
|
||||
)
|
||||
)
|
||||
for extension, mimetype in utils.EXTENSION_TO_MIMETYPE.items():
|
||||
qs = matching.filter(source__endswith='.{}'.format(extension))
|
||||
qs = matching.filter(source__endswith=".{}".format(extension))
|
||||
self.stdout.write(
|
||||
'[mimetypes] setting {} {} files to {}'.format(
|
||||
"[mimetypes] setting {} {} files to {}".format(
|
||||
qs.count(), extension, mimetype
|
||||
))
|
||||
)
|
||||
)
|
||||
if not dry_run:
|
||||
self.stdout.write('[mimetypes] commiting...')
|
||||
self.stdout.write("[mimetypes] commiting...")
|
||||
qs.update(mimetype=mimetype)
|
||||
|
||||
def fix_file_data(self, dry_run, **kwargs):
|
||||
self.stdout.write('Fixing missing bitrate or length...')
|
||||
self.stdout.write("Fixing missing bitrate or length...")
|
||||
matching = models.TrackFile.objects.filter(
|
||||
Q(bitrate__isnull=True) | Q(duration__isnull=True))
|
||||
Q(bitrate__isnull=True) | Q(duration__isnull=True)
|
||||
)
|
||||
total = matching.count()
|
||||
self.stdout.write(
|
||||
'[bitrate/length] {} entries found with missing values'.format(
|
||||
total))
|
||||
"[bitrate/length] {} entries found with missing values".format(total)
|
||||
)
|
||||
if dry_run:
|
||||
return
|
||||
for i, tf in enumerate(matching.only('audio_file')):
|
||||
for i, tf in enumerate(matching.only("audio_file")):
|
||||
self.stdout.write(
|
||||
'[bitrate/length] {}/{} fixing file #{}'.format(
|
||||
i+1, total, tf.pk
|
||||
))
|
||||
"[bitrate/length] {}/{} fixing file #{}".format(i + 1, total, tf.pk)
|
||||
)
|
||||
|
||||
try:
|
||||
audio_file = tf.get_audio_file()
|
||||
if audio_file:
|
||||
with audio_file as f:
|
||||
data = utils.get_audio_file_data(audio_file)
|
||||
tf.bitrate = data['bitrate']
|
||||
tf.duration = data['length']
|
||||
tf.save(update_fields=['duration', 'bitrate'])
|
||||
tf.bitrate = data["bitrate"]
|
||||
tf.duration = data["length"]
|
||||
tf.save(update_fields=["duration", "bitrate"])
|
||||
else:
|
||||
self.stderr.write('[bitrate/length] no file found')
|
||||
self.stderr.write("[bitrate/length] no file found")
|
||||
except Exception as e:
|
||||
self.stderr.write(
|
||||
'[bitrate/length] error with file #{}: {}'.format(
|
||||
tf.pk, str(e)
|
||||
)
|
||||
"[bitrate/length] error with file #{}: {}".format(tf.pk, str(e))
|
||||
)
|
||||
|
||||
def fix_file_size(self, dry_run, **kwargs):
|
||||
self.stdout.write('Fixing missing size...')
|
||||
self.stdout.write("Fixing missing size...")
|
||||
matching = models.TrackFile.objects.filter(size__isnull=True)
|
||||
total = matching.count()
|
||||
self.stdout.write(
|
||||
'[size] {} entries found with missing values'.format(total))
|
||||
self.stdout.write("[size] {} entries found with missing values".format(total))
|
||||
if dry_run:
|
||||
return
|
||||
for i, tf in enumerate(matching.only('size')):
|
||||
for i, tf in enumerate(matching.only("size")):
|
||||
self.stdout.write(
|
||||
'[size] {}/{} fixing file #{}'.format(
|
||||
i+1, total, tf.pk
|
||||
))
|
||||
"[size] {}/{} fixing file #{}".format(i + 1, total, tf.pk)
|
||||
)
|
||||
|
||||
try:
|
||||
tf.size = tf.get_file_size()
|
||||
tf.save(update_fields=['size'])
|
||||
tf.save(update_fields=["size"])
|
||||
except Exception as e:
|
||||
self.stderr.write(
|
||||
'[size] error with file #{}: {}'.format(
|
||||
tf.pk, str(e)
|
||||
)
|
||||
"[size] error with file #{}: {}".format(tf.pk, str(e))
|
||||
)
|
||||
|
|
|
@ -14,21 +14,17 @@ class UnsupportedTag(KeyError):
|
|||
|
||||
|
||||
def get_id3_tag(f, k):
|
||||
if k == 'pictures':
|
||||
return f.tags.getall('APIC')
|
||||
if k == "pictures":
|
||||
return f.tags.getall("APIC")
|
||||
# First we try to grab the standard key
|
||||
try:
|
||||
return f.tags[k].text[0]
|
||||
except KeyError:
|
||||
pass
|
||||
# then we fallback on parsing non standard tags
|
||||
all_tags = f.tags.getall('TXXX')
|
||||
all_tags = f.tags.getall("TXXX")
|
||||
try:
|
||||
matches = [
|
||||
t
|
||||
for t in all_tags
|
||||
if t.desc.lower() == k.lower()
|
||||
]
|
||||
matches = [t for t in all_tags if t.desc.lower() == k.lower()]
|
||||
return matches[0].text[0]
|
||||
except (KeyError, IndexError):
|
||||
raise TagNotFound(k)
|
||||
|
@ -37,17 +33,19 @@ def get_id3_tag(f, k):
|
|||
def clean_id3_pictures(apic):
|
||||
pictures = []
|
||||
for p in list(apic):
|
||||
pictures.append({
|
||||
'mimetype': p.mime,
|
||||
'content': p.data,
|
||||
'description': p.desc,
|
||||
'type': p.type.real,
|
||||
})
|
||||
pictures.append(
|
||||
{
|
||||
"mimetype": p.mime,
|
||||
"content": p.data,
|
||||
"description": p.desc,
|
||||
"type": p.type.real,
|
||||
}
|
||||
)
|
||||
return pictures
|
||||
|
||||
|
||||
def get_flac_tag(f, k):
|
||||
if k == 'pictures':
|
||||
if k == "pictures":
|
||||
return f.pictures
|
||||
try:
|
||||
return f.get(k, [])[0]
|
||||
|
@ -58,22 +56,22 @@ def get_flac_tag(f, k):
|
|||
def clean_flac_pictures(apic):
|
||||
pictures = []
|
||||
for p in list(apic):
|
||||
pictures.append({
|
||||
'mimetype': p.mime,
|
||||
'content': p.data,
|
||||
'description': p.desc,
|
||||
'type': p.type.real,
|
||||
})
|
||||
pictures.append(
|
||||
{
|
||||
"mimetype": p.mime,
|
||||
"content": p.data,
|
||||
"description": p.desc,
|
||||
"type": p.type.real,
|
||||
}
|
||||
)
|
||||
return pictures
|
||||
|
||||
|
||||
def get_mp3_recording_id(f, k):
|
||||
try:
|
||||
return [
|
||||
t
|
||||
for t in f.tags.getall('UFID')
|
||||
if 'musicbrainz.org' in t.owner
|
||||
][0].data.decode('utf-8')
|
||||
return [t for t in f.tags.getall("UFID") if "musicbrainz.org" in t.owner][
|
||||
0
|
||||
].data.decode("utf-8")
|
||||
except IndexError:
|
||||
raise TagNotFound(k)
|
||||
|
||||
|
@ -86,18 +84,17 @@ def convert_track_number(v):
|
|||
pass
|
||||
|
||||
try:
|
||||
return int(v.split('/')[0])
|
||||
return int(v.split("/")[0])
|
||||
except (ValueError, AttributeError, IndexError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class FirstUUIDField(forms.UUIDField):
|
||||
def to_python(self, value):
|
||||
try:
|
||||
# sometimes, Picard leaves to uuids in the field, separated
|
||||
# by a slash
|
||||
value = value.split('/')[0]
|
||||
value = value.split("/")[0]
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
pass
|
||||
|
||||
|
@ -105,150 +102,119 @@ class FirstUUIDField(forms.UUIDField):
|
|||
|
||||
|
||||
VALIDATION = {
|
||||
'musicbrainz_artistid': FirstUUIDField(),
|
||||
'musicbrainz_albumid': FirstUUIDField(),
|
||||
'musicbrainz_recordingid': FirstUUIDField(),
|
||||
"musicbrainz_artistid": FirstUUIDField(),
|
||||
"musicbrainz_albumid": FirstUUIDField(),
|
||||
"musicbrainz_recordingid": FirstUUIDField(),
|
||||
}
|
||||
|
||||
CONF = {
|
||||
'OggVorbis': {
|
||||
'getter': lambda f, k: f[k][0],
|
||||
'fields': {
|
||||
'track_number': {
|
||||
'field': 'TRACKNUMBER',
|
||||
'to_application': convert_track_number
|
||||
"OggVorbis": {
|
||||
"getter": lambda f, k: f[k][0],
|
||||
"fields": {
|
||||
"track_number": {
|
||||
"field": "TRACKNUMBER",
|
||||
"to_application": convert_track_number,
|
||||
},
|
||||
'title': {},
|
||||
'artist': {},
|
||||
'album': {},
|
||||
'date': {
|
||||
'field': 'date',
|
||||
'to_application': lambda v: arrow.get(v).date()
|
||||
},
|
||||
'musicbrainz_albumid': {},
|
||||
'musicbrainz_artistid': {},
|
||||
'musicbrainz_recordingid': {
|
||||
'field': 'musicbrainz_trackid'
|
||||
},
|
||||
}
|
||||
"title": {},
|
||||
"artist": {},
|
||||
"album": {},
|
||||
"date": {"field": "date", "to_application": lambda v: arrow.get(v).date()},
|
||||
"musicbrainz_albumid": {},
|
||||
"musicbrainz_artistid": {},
|
||||
"musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
|
||||
},
|
||||
},
|
||||
'OggTheora': {
|
||||
'getter': lambda f, k: f[k][0],
|
||||
'fields': {
|
||||
'track_number': {
|
||||
'field': 'TRACKNUMBER',
|
||||
'to_application': convert_track_number
|
||||
"OggTheora": {
|
||||
"getter": lambda f, k: f[k][0],
|
||||
"fields": {
|
||||
"track_number": {
|
||||
"field": "TRACKNUMBER",
|
||||
"to_application": convert_track_number,
|
||||
},
|
||||
'title': {},
|
||||
'artist': {},
|
||||
'album': {},
|
||||
'date': {
|
||||
'field': 'date',
|
||||
'to_application': lambda v: arrow.get(v).date()
|
||||
},
|
||||
'musicbrainz_albumid': {
|
||||
'field': 'MusicBrainz Album Id'
|
||||
},
|
||||
'musicbrainz_artistid': {
|
||||
'field': 'MusicBrainz Artist Id'
|
||||
},
|
||||
'musicbrainz_recordingid': {
|
||||
'field': 'MusicBrainz Track Id'
|
||||
},
|
||||
}
|
||||
"title": {},
|
||||
"artist": {},
|
||||
"album": {},
|
||||
"date": {"field": "date", "to_application": lambda v: arrow.get(v).date()},
|
||||
"musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
|
||||
"musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
|
||||
"musicbrainz_recordingid": {"field": "MusicBrainz Track Id"},
|
||||
},
|
||||
},
|
||||
'MP3': {
|
||||
'getter': get_id3_tag,
|
||||
'clean_pictures': clean_id3_pictures,
|
||||
'fields': {
|
||||
'track_number': {
|
||||
'field': 'TRCK',
|
||||
'to_application': convert_track_number
|
||||
"MP3": {
|
||||
"getter": get_id3_tag,
|
||||
"clean_pictures": clean_id3_pictures,
|
||||
"fields": {
|
||||
"track_number": {"field": "TRCK", "to_application": convert_track_number},
|
||||
"title": {"field": "TIT2"},
|
||||
"artist": {"field": "TPE1"},
|
||||
"album": {"field": "TALB"},
|
||||
"date": {
|
||||
"field": "TDRC",
|
||||
"to_application": lambda v: arrow.get(str(v)).date(),
|
||||
},
|
||||
'title': {
|
||||
'field': 'TIT2'
|
||||
"musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
|
||||
"musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
|
||||
"musicbrainz_recordingid": {
|
||||
"field": "UFID",
|
||||
"getter": get_mp3_recording_id,
|
||||
},
|
||||
'artist': {
|
||||
'field': 'TPE1'
|
||||
},
|
||||
'album': {
|
||||
'field': 'TALB'
|
||||
},
|
||||
'date': {
|
||||
'field': 'TDRC',
|
||||
'to_application': lambda v: arrow.get(str(v)).date()
|
||||
},
|
||||
'musicbrainz_albumid': {
|
||||
'field': 'MusicBrainz Album Id'
|
||||
},
|
||||
'musicbrainz_artistid': {
|
||||
'field': 'MusicBrainz Artist Id'
|
||||
},
|
||||
'musicbrainz_recordingid': {
|
||||
'field': 'UFID',
|
||||
'getter': get_mp3_recording_id,
|
||||
},
|
||||
'pictures': {},
|
||||
}
|
||||
"pictures": {},
|
||||
},
|
||||
},
|
||||
'FLAC': {
|
||||
'getter': get_flac_tag,
|
||||
'clean_pictures': clean_flac_pictures,
|
||||
'fields': {
|
||||
'track_number': {
|
||||
'field': 'tracknumber',
|
||||
'to_application': convert_track_number
|
||||
"FLAC": {
|
||||
"getter": get_flac_tag,
|
||||
"clean_pictures": clean_flac_pictures,
|
||||
"fields": {
|
||||
"track_number": {
|
||||
"field": "tracknumber",
|
||||
"to_application": convert_track_number,
|
||||
},
|
||||
'title': {},
|
||||
'artist': {},
|
||||
'album': {},
|
||||
'date': {
|
||||
'field': 'date',
|
||||
'to_application': lambda v: arrow.get(str(v)).date()
|
||||
"title": {},
|
||||
"artist": {},
|
||||
"album": {},
|
||||
"date": {
|
||||
"field": "date",
|
||||
"to_application": lambda v: arrow.get(str(v)).date(),
|
||||
},
|
||||
'musicbrainz_albumid': {},
|
||||
'musicbrainz_artistid': {},
|
||||
'musicbrainz_recordingid': {
|
||||
'field': 'musicbrainz_trackid'
|
||||
},
|
||||
'test': {},
|
||||
'pictures': {},
|
||||
}
|
||||
"musicbrainz_albumid": {},
|
||||
"musicbrainz_artistid": {},
|
||||
"musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
|
||||
"test": {},
|
||||
"pictures": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Metadata(object):
|
||||
|
||||
def __init__(self, path):
|
||||
self._file = mutagen.File(path)
|
||||
if self._file is None:
|
||||
raise ValueError('Cannot parse metadata from {}'.format(path))
|
||||
raise ValueError("Cannot parse metadata from {}".format(path))
|
||||
ft = self.get_file_type(self._file)
|
||||
try:
|
||||
self._conf = CONF[ft]
|
||||
except KeyError:
|
||||
raise ValueError('Unsupported format {}'.format(ft))
|
||||
raise ValueError("Unsupported format {}".format(ft))
|
||||
|
||||
def get_file_type(self, f):
|
||||
return f.__class__.__name__
|
||||
|
||||
def get(self, key, default=NODEFAULT):
|
||||
try:
|
||||
field_conf = self._conf['fields'][key]
|
||||
field_conf = self._conf["fields"][key]
|
||||
except KeyError:
|
||||
raise UnsupportedTag(
|
||||
'{} is not supported for this file format'.format(key))
|
||||
real_key = field_conf.get('field', key)
|
||||
raise UnsupportedTag("{} is not supported for this file format".format(key))
|
||||
real_key = field_conf.get("field", key)
|
||||
try:
|
||||
getter = field_conf.get('getter', self._conf['getter'])
|
||||
getter = field_conf.get("getter", self._conf["getter"])
|
||||
v = getter(self._file, real_key)
|
||||
except KeyError:
|
||||
if default == NODEFAULT:
|
||||
raise TagNotFound(real_key)
|
||||
return default
|
||||
|
||||
converter = field_conf.get('to_application')
|
||||
converter = field_conf.get("to_application")
|
||||
if converter:
|
||||
v = converter(v)
|
||||
field = VALIDATION.get(key)
|
||||
|
@ -256,15 +222,15 @@ class Metadata(object):
|
|||
v = field.to_python(v)
|
||||
return v
|
||||
|
||||
def get_picture(self, picture_type='cover_front'):
|
||||
def get_picture(self, picture_type="cover_front"):
|
||||
ptype = getattr(mutagen.id3.PictureType, picture_type.upper())
|
||||
try:
|
||||
pictures = self.get('pictures')
|
||||
pictures = self.get("pictures")
|
||||
except (UnsupportedTag, TagNotFound):
|
||||
return
|
||||
|
||||
cleaner = self._conf.get('clean_pictures', lambda v: v)
|
||||
cleaner = self._conf.get("clean_pictures", lambda v: v)
|
||||
pictures = cleaner(pictures)
|
||||
for p in pictures:
|
||||
if p['type'] == ptype:
|
||||
if p["type"] == ptype:
|
||||
return p
|
||||
|
|
|
@ -8,82 +8,183 @@ import django.utils.timezone
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Album',
|
||||
name="Album",
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
|
||||
('mbid', models.UUIDField(editable=False, blank=True, null=True)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('release_date', models.DateField()),
|
||||
('type', models.CharField(default='album', choices=[('album', 'Album')], max_length=30)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
primary_key=True,
|
||||
auto_created=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("mbid", models.UUIDField(editable=False, blank=True, null=True)),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("title", models.CharField(max_length=255)),
|
||||
("release_date", models.DateField()),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
default="album", choices=[("album", "Album")], max_length=30
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
options={"abstract": False},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Artist',
|
||||
name="Artist",
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
|
||||
('mbid', models.UUIDField(editable=False, blank=True, null=True)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
primary_key=True,
|
||||
auto_created=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("mbid", models.UUIDField(editable=False, blank=True, null=True)),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
options={"abstract": False},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ImportBatch',
|
||||
name="ImportBatch",
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('submitted_by', models.ForeignKey(related_name='imports', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
primary_key=True,
|
||||
auto_created=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
(
|
||||
"submitted_by",
|
||||
models.ForeignKey(
|
||||
related_name="imports",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ImportJob',
|
||||
name="ImportJob",
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
|
||||
('source', models.URLField()),
|
||||
('mbid', models.UUIDField(editable=False)),
|
||||
('status', models.CharField(default='pending', choices=[('pending', 'Pending'), ('finished', 'finished')], max_length=30)),
|
||||
('batch', models.ForeignKey(related_name='jobs', to='music.ImportBatch', on_delete=models.CASCADE)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
primary_key=True,
|
||||
auto_created=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("source", models.URLField()),
|
||||
("mbid", models.UUIDField(editable=False)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
default="pending",
|
||||
choices=[("pending", "Pending"), ("finished", "finished")],
|
||||
max_length=30,
|
||||
),
|
||||
),
|
||||
(
|
||||
"batch",
|
||||
models.ForeignKey(
|
||||
related_name="jobs",
|
||||
to="music.ImportBatch",
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Track',
|
||||
name="Track",
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
|
||||
('mbid', models.UUIDField(editable=False, blank=True, null=True)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('album', models.ForeignKey(related_name='tracks', blank=True, null=True, to='music.Album', on_delete=models.CASCADE)),
|
||||
('artist', models.ForeignKey(related_name='tracks', to='music.Artist', on_delete=models.CASCADE)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
primary_key=True,
|
||||
auto_created=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("mbid", models.UUIDField(editable=False, blank=True, null=True)),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("title", models.CharField(max_length=255)),
|
||||
(
|
||||
"album",
|
||||
models.ForeignKey(
|
||||
related_name="tracks",
|
||||
blank=True,
|
||||
null=True,
|
||||
to="music.Album",
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
(
|
||||
"artist",
|
||||
models.ForeignKey(
|
||||
related_name="tracks",
|
||||
to="music.Artist",
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
options={"abstract": False},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TrackFile',
|
||||
name="TrackFile",
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
|
||||
('audio_file', models.FileField(upload_to='tracks')),
|
||||
('source', models.URLField(blank=True, null=True)),
|
||||
('duration', models.IntegerField(blank=True, null=True)),
|
||||
('track', models.ForeignKey(related_name='files', to='music.Track', on_delete=models.CASCADE)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
primary_key=True,
|
||||
auto_created=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("audio_file", models.FileField(upload_to="tracks")),
|
||||
("source", models.URLField(blank=True, null=True)),
|
||||
("duration", models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
"track",
|
||||
models.ForeignKey(
|
||||
related_name="files", to="music.Track", on_delete=models.CASCADE
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='album',
|
||||
name='artist',
|
||||
field=models.ForeignKey(related_name='albums', to='music.Artist', on_delete=models.CASCADE),
|
||||
model_name="album",
|
||||
name="artist",
|
||||
field=models.ForeignKey(
|
||||
related_name="albums", to="music.Artist", on_delete=models.CASCADE
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,35 +6,31 @@ from django.db import migrations, models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0001_initial'),
|
||||
]
|
||||
dependencies = [("music", "0001_initial")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='album',
|
||||
options={'ordering': ['-creation_date']},
|
||||
name="album", options={"ordering": ["-creation_date"]}
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='artist',
|
||||
options={'ordering': ['-creation_date']},
|
||||
name="artist", options={"ordering": ["-creation_date"]}
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='importbatch',
|
||||
options={'ordering': ['-creation_date']},
|
||||
name="importbatch", options={"ordering": ["-creation_date"]}
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='track',
|
||||
options={'ordering': ['-creation_date']},
|
||||
name="track", options={"ordering": ["-creation_date"]}
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='album',
|
||||
name='cover',
|
||||
field=models.ImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True),
|
||||
model_name="album",
|
||||
name="cover",
|
||||
field=models.ImageField(
|
||||
upload_to="albums/covers/%Y/%m/%d", null=True, blank=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trackfile',
|
||||
name='audio_file',
|
||||
field=models.FileField(upload_to='tracks/%Y/%m/%d'),
|
||||
model_name="trackfile",
|
||||
name="audio_file",
|
||||
field=models.FileField(upload_to="tracks/%Y/%m/%d"),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,14 +6,10 @@ from django.db import migrations, models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0002_auto_20151215_1645'),
|
||||
]
|
||||
dependencies = [("music", "0002_auto_20151215_1645")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='release_date',
|
||||
field=models.DateField(null=True),
|
||||
),
|
||||
model_name="album", name="release_date", field=models.DateField(null=True)
|
||||
)
|
||||
]
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue