Blacked the code

merge-requests/251/head
Eliot Berriot 2018-06-09 15:36:16 +02:00
rodzic b6fc0051fa
commit 62ca3bd736
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: DD6965E2476E5C27
279 zmienionych plików z 8861 dodań i 9527 usunięć

Wyświetl plik

@ -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"])

Wyświetl plik

@ -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)]
)
)
}
)

Wyświetl plik

@ -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
)

Wyświetl plik

@ -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]

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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))]

Wyświetl plik

@ -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()

Wyświetl plik

@ -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(".")
]
)

Wyświetl plik

@ -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()

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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]

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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()

Wyświetl plik

@ -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)."
)

Wyświetl plik

@ -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):

Wyświetl plik

@ -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

Wyświetl plik

@ -7,6 +7,4 @@ class Migration(migrations.Migration):
dependencies = []
operations = [
UnaccentExtension()
]
operations = [UnaccentExtension()]

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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})

Wyświetl plik

@ -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")

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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):

Wyświetl plik

@ -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())],
)
]

Wyświetl plik

@ -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)]

Wyświetl plik

@ -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",
),
),
]

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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}
)

Wyświetl plik

@ -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"]

Wyświetl plik

@ -12,4 +12,4 @@ class TrackFavorite(factory.django.DjangoModelFactory):
user = factory.SubFactory(UserFactory)
class Meta:
model = 'favorites.TrackFavorite'
model = "favorites.TrackFavorite"

Wyświetl plik

@ -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")])
),
]

Wyświetl plik

@ -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)

Wyświetl plik

@ -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")

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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()}

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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."
)

Wyświetl plik

@ -1,5 +1,3 @@
class MalformedPayload(ValueError):
pass

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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)),
],
),
)
]

Wyświetl plik

@ -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")}
)
]

Wyświetl plik

@ -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")}
),
]

Wyświetl plik

@ -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"),
]

Wyświetl plik

@ -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,
),
),
]

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -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)

Wyświetl plik

@ -2,4 +2,4 @@ from rest_framework import parsers
class ActivityParser(parsers.JSONParser):
media_type = 'application/activity+json'
media_type = "application/activity+json"

Wyświetl plik

@ -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()

Wyświetl plik

@ -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"

Wyświetl plik

@ -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,
)

Wyświetl plik

@ -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]

Wyświetl plik

@ -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"))
]

Wyświetl plik

@ -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

Wyświetl plik

@ -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()

Wyświetl plik

@ -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)

Wyświetl plik

@ -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}
)

Wyświetl plik

@ -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"]

Wyświetl plik

@ -11,4 +11,4 @@ class ListeningFactory(factory.django.DjangoModelFactory):
track = factory.SubFactory(factories.TrackFactory)
class Meta:
model = 'history.Listening'
model = "history.Listening"

Wyświetl plik

@ -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",)},
)
]

Wyświetl plik

@ -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"
),
]

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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"])

Wyświetl plik

@ -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."
)

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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"]

Wyświetl plik

@ -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

Wyświetl plik

@ -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"))
]

Wyświetl plik

@ -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()

Wyświetl plik

@ -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"]

Wyświetl plik

@ -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"

Wyświetl plik

@ -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()

Wyświetl plik

@ -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"]

Wyświetl plik

@ -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}

Wyświetl plik

@ -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:

Wyświetl plik

@ -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))
)

Wyświetl plik

@ -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

Wyświetl plik

@ -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
),
),
]

Wyświetl plik

@ -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"),
),
]

Wyświetl plik

@ -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