diff --git a/.env.dev b/.env.dev index d4833ab01..f13026e26 100644 --- a/.env.dev +++ b/.env.dev @@ -11,3 +11,4 @@ VUE_PORT=8080 MUSIC_DIRECTORY_PATH=/music BROWSABLE_API_ENABLED=True FORWARDED_PROTO=http +LDAP_ENABLED=False diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 57b7dfc7f..c7a43c940 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -148,6 +148,8 @@ test_api: - branches before_script: - cd api + - apt-get update + - grep "^[^#;]" requirements.apt | grep -Fv "python3-dev" | xargs apt-get install -y --no-install-recommends - pip install -r requirements/base.txt - pip install -r requirements/local.txt - pip install -r requirements/test.txt diff --git a/.gitlab/merge_request_templates/Merge request.md b/.gitlab/merge_request_templates/Merge request.md new file mode 100644 index 000000000..a30b6856a --- /dev/null +++ b/.gitlab/merge_request_templates/Merge request.md @@ -0,0 +1,6 @@ +Related issue: #XXX + +This Merge Request includes: + +- [ ] Tests +- [ ] A changelog fragment (cf https://docs.funkwhale.audio/contributing.html#changelog-management) diff --git a/CHANGELOG b/CHANGELOG index 007671930..bff6dff2d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,200 @@ This changelog is viewable on the web at https://docs.funkwhale.audio/changelog. .. towncrier +0.17 (2018-10-07) +----------------- + +Per user libraries +^^^^^^^^^^^^^^^^^^ + +This release contains a big change in music management. This has a lot of impact +on how Funkwhale behaves, and you should have a look at +https://docs.funkwhale.audio/upgrading/0.17.html for information +about what changed and how to migrate. + + +Features: + +- Per user libraries (#463, also fixes #160 and #147) +- Authentication using a LDAP directory (#194) + + +Enhancements: + +- Add configuration option to set Musicbrainz hostname +- Add sign up link in the sidebar (#408) +- Added a library widget to display libraries associated with a track, album + and artist (#551) +- Ensure from_activity field is not required in django's admin (#546) +- Move setting link from profile page to the sidebar (#406) +- Simplified and less error-prone nginx setup (#358) + +Bugfixes: + +- Do not restart current song when rordering queue, deleting tracks from queue + or adding tracks to queue (#464) +- Fix broken icons in playlist editor (#515) +- Fixed a few untranslated strings (#559) +- Fixed splitted album when importing from federation (#346) +- Fixed toggle mute in volume bar does not restore previous volume level (#514) +- Fixed wrong env file URL and display bugs in deployment documentation (#520) +- Fixed wrong title in PlayButton (#435) +- Remove transparency on artist page button (#517) +- Set sane width default for ui cards and center play button (#530) +- Updated wrong icon and copy in play button dropdown (#436) + + +Documentation: + +- Fixed wrong URLs for docker / nginx files in documentation (#537) + + +Other: + +- Added a merge request template and more documentation about the changelog + + +Using a LDAP directory to authenticate to your Funkwhale instance +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Funkwhale now support LDAP as an authentication source: you can configure +your instance to delegate login to a LDAP directory, which is especially +useful when you have an existing directory and don't want to manage users +manually. + +You can use this authentication backend side by side with the classic one. + +Have a look at https://docs.funkwhale.audio/installation/ldap.html +for detailed instructions on how to set this up. + + +Simplified nginx setup [Docker: Manual action required] +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We've received a lot of user feedback regarding our installation process, +and it seems the proxy part is the one which is the most confusing and difficult. +Unfortunately, this is also the one where errors and mistakes can completely break +the application. + +To make things easier for everyone, we now offer a simplified deployment +process for the reverse proxy part. This will make upgrade of the proxy configuration +significantly easier on docker deployments. + +On non-docker instances, you have nothing to do. + +If you have a dockerized instance, here is the upgrade path. + +First, tweak your .env file:: + + # remove the FUNKWHALE_URL variable + # and add the next variables + FUNKWHALE_HOSTNAME=yourdomain.funkwhale + FUNKWHALE_PROTOCOL=https + + # add the following variable, matching the path your app is deployed + # leaving the default should work fine if you deployed using the same + # paths as the documentation + FUNKWHALE_FRONTEND_PATH=/srv/funkwhale/front/dist + +Then, add the following block at the end of your docker-compose.yml file:: + + # existing services + api: + ... + celeryworker: + ... + + # new service + nginx: + image: nginx + env_file: + - .env + environment: + # Override those variables in your .env file if needed + - "NGINX_MAX_BODY_SIZE=${NGINX_MAX_BODY_SIZE-30M}" + volumes: + - "./nginx/funkwhale.template:/etc/nginx/conf.d/funkwhale.template:ro" + - "./nginx/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro" + - "${MUSIC_DIRECTORY_SERVE_PATH-/srv/funkwhale/data/music}:${MUSIC_DIRECTORY_SERVE_PATH-/srv/funkwhale/data/music}:ro" + - "${MEDIA_ROOT}:${MEDIA_ROOT}:ro" + - "${STATIC_ROOT}:${STATIC_ROOT}:ro" + - "${FUNKWHALE_FRONTEND_PATH}:/frontend:ro" + ports: + # override those variables in your .env file if needed + - "${FUNKWHALE_API_IP}:${FUNKWHALE_API_PORT}:80" + command: > + sh -c "envsubst \"`env | awk -F = '{printf \" $$%s\", $$1}'`\" + < /etc/nginx/conf.d/funkwhale.template + > /etc/nginx/conf.d/default.conf + && cat /etc/nginx/conf.d/default.conf + && nginx -g 'daemon off;'" + links: + - api + +By doing that, you'll enable a dockerized nginx that will automatically be +configured to serve your Funkwhale instance. + +Download the required configuration files for the nginx container: + +.. parsed-literal:: + + cd /srv/funkwhale + mkdir nginx + curl -L -o nginx/funkwhale.template "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/docker.nginx.template" + curl -L -o nginx/funkwhale_proxy.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale_proxy.conf" + +Update the funkwhale.conf configuration of your server's reverse-proxy:: + + # the file should match something like that, upgrade all variables + # between ${} to match the ones in your .env file, + # and your SSL configuration if you're not using let's encrypt + # The important thing is that you only have a single location block + # that proxies everything to your dockerized nginx. + + sudo nano /etc/nginx/sites-enabled/funkwhale.conf + upstream fw { + # depending on your setup, you may want to update this + server ${FUNKWHALE_API_IP}:${FUNKWHALE_API_PORT}; + } + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + server { + listen 80; + listen [::]:80; + server_name ${FUNKWHALE_HOSTNAME}; + location / { return 301 https://$host$request_uri; } + } + server { + listen 443 ssl; + listen [::]:443 ssl; + server_name ${FUNKWHALE_HOSTNAME}; + + # TLS + ssl_protocols TLSv1.2; + ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_certificate /etc/letsencrypt/live/${FUNKWHALE_HOSTNAME}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${FUNKWHALE_HOSTNAME}/privkey.pem; + + # HSTS + add_header Strict-Transport-Security "max-age=31536000"; + + location / { + include /etc/nginx/funkwhale_proxy.conf; + proxy_pass http://fw/; + } + } + +Check that your configuration is valid then reload: + + sudo nginx -t + sudo systemctl reload nginx + + 0.16.3 (2018-08-21) ------------------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 86832841d..0c2bd5bbd 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -249,6 +249,7 @@ Then, in separate terminals, you can setup as many different instances as you need:: export COMPOSE_PROJECT_NAME=node2 + export VUE_PORT=1234 # this has to be unique for each instance docker-compose -f dev.yml run --rm api python manage.py migrate docker-compose -f dev.yml run --rm api python manage.py createsuperuser docker-compose -f dev.yml up nginx api front nginx api celeryworker @@ -285,6 +286,62 @@ Typical workflow for a contribution 8. Create your merge request 9. Take a step back and enjoy, we're really grateful you did all of this and took the time to contribute! +Changelog management +-------------------- + +To ensure we have extensive and well-structured changelog, any significant +work such as closing an issue must include a changelog fragment. Small changes +may include a changelog fragment as well but this is not mandatory. If you're not +sure about what to do, do not panic, open your merge request normally and we'll +figure everything during the review ;) + +Changelog fragments are text files that can contain one or multiple lines +that describe the changes occuring in a bunch of commits. Those files reside +in ``changes/changelog.d``. + +Content +^^^^^^^ + +A typical fragment looks like that: + + Fixed broken audio player on Chrome 42 for ogg files (#567) + +If the work fixes one or more issues, the issue number should be included at the +end of the fragment (``(#567)`` is the issue number in the previous example. + +If your work is not related to a specific issue, use the merge request +identifier instead, like this: + + Fixed a typo in landing page copy (!342) + +Naming +^^^^^^ + +Fragment files should respect the following naming pattern: ``changes/changelog.d/.``. +Name can be anything describing your work, or simply the identifier of the issue number you are fixing. +Category can be one of: + +- ``feature``: for new features +- ``enhancement``: for enhancements on existing features +- ``bugfix``: for bugfixes +- ``doc``: for documentation +- ``i18n``: for internationalization-related work +- ``misc``: for anything else + +Shortcuts +^^^^^^^^^ + +Here is a shortcut you can use/adapt to easily create new fragments from command-line: + +.. code-block:: bash + + issue="42" + content="Fixed an overflowing issue on small resolutions (#$issue)" + category="bugfix" + echo $content > changes/changelog.d/$issue.$category + +You can of course create fragments by hand in your text editor, or from Gitlab's +interface as well. Internationalization -------------------- diff --git a/api/config/api_urls.py b/api/config/api_urls.py index 9f87a7af3..fd076c8e2 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -14,12 +14,11 @@ 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"uploads", views.UploadViewSet, "uploads") +router.register(r"libraries", views.LibraryViewSet, "libraries") +router.register(r"listen", views.ListenViewSet, "listen") 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" @@ -66,10 +65,6 @@ v1_patterns += [ 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"), ] diff --git a/api/config/routing.py b/api/config/routing.py index fa25aad07..13a67cd1e 100644 --- a/api/config/routing.py +++ b/api/config/routing.py @@ -8,9 +8,7 @@ application = ProtocolTypeRouter( { # Empty for now (http->django views is added by default) "websocket": TokenAuthMiddleware( - URLRouter( - [url("^api/v1/instance/activity$", consumers.InstanceActivityConsumer)] - ) + URLRouter([url("^api/v1/activity$", consumers.InstanceActivityConsumer)]) ) } ) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 114157978..4759d7aab 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -125,8 +125,6 @@ LOCAL_APPS = ( "funkwhale_api.radios", "funkwhale_api.history", "funkwhale_api.playlists", - "funkwhale_api.providers.audiofile", - "funkwhale_api.providers.youtube", "funkwhale_api.providers.acoustid", "funkwhale_api.subsonic", ) @@ -280,7 +278,7 @@ 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/") - +FILE_UPLOAD_PERMISSIONS = 0o644 # URL Configuration # ------------------------------------------------------------------------------ ROOT_URLCONF = "config.urls" @@ -310,6 +308,71 @@ AUTH_USER_MODEL = "users.User" LOGIN_REDIRECT_URL = "users:redirect" LOGIN_URL = "account_login" +# LDAP AUTHENTICATION CONFIGURATION +# ------------------------------------------------------------------------------ +AUTH_LDAP_ENABLED = env.bool("LDAP_ENABLED", default=False) +if AUTH_LDAP_ENABLED: + + # Import the LDAP modules here; this way, we don't need the dependency unless someone + # actually enables the LDAP support + import ldap + from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion, GroupOfNamesType + + # Add LDAP to the authentication backends + AUTHENTICATION_BACKENDS += ("django_auth_ldap.backend.LDAPBackend",) + + # Basic configuration + AUTH_LDAP_SERVER_URI = env("LDAP_SERVER_URI") + AUTH_LDAP_BIND_DN = env("LDAP_BIND_DN", default="") + AUTH_LDAP_BIND_PASSWORD = env("LDAP_BIND_PASSWORD", default="") + AUTH_LDAP_SEARCH_FILTER = env("LDAP_SEARCH_FILTER", default="(uid={0})").format( + "%(user)s" + ) + AUTH_LDAP_START_TLS = env.bool("LDAP_START_TLS", default=False) + + DEFAULT_USER_ATTR_MAP = [ + "first_name:givenName", + "last_name:sn", + "username:cn", + "email:mail", + ] + LDAP_USER_ATTR_MAP = env.list("LDAP_USER_ATTR_MAP", default=DEFAULT_USER_ATTR_MAP) + AUTH_LDAP_USER_ATTR_MAP = {} + for m in LDAP_USER_ATTR_MAP: + funkwhale_field, ldap_field = m.split(":") + AUTH_LDAP_USER_ATTR_MAP[funkwhale_field.strip()] = ldap_field.strip() + + # Determine root DN supporting multiple root DNs + AUTH_LDAP_ROOT_DN = env("LDAP_ROOT_DN") + AUTH_LDAP_ROOT_DN_LIST = [] + for ROOT_DN in AUTH_LDAP_ROOT_DN.split(): + AUTH_LDAP_ROOT_DN_LIST.append( + LDAPSearch(ROOT_DN, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER) + ) + # Search for the user in all the root DNs + AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*AUTH_LDAP_ROOT_DN_LIST) + + # Search for group types + LDAP_GROUP_DN = env("LDAP_GROUP_DN", default="") + if LDAP_GROUP_DN: + AUTH_LDAP_GROUP_DN = LDAP_GROUP_DN + # Get filter + AUTH_LDAP_GROUP_FILTER = env("LDAP_GROUP_FILER", default="") + # Search for the group in the specified DN + AUTH_LDAP_GROUP_SEARCH = LDAPSearch( + AUTH_LDAP_GROUP_DN, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_FILTER + ) + AUTH_LDAP_GROUP_TYPE = GroupOfNamesType() + + # Configure basic group support + LDAP_REQUIRE_GROUP = env("LDAP_REQUIRE_GROUP", default="") + if LDAP_REQUIRE_GROUP: + AUTH_LDAP_REQUIRE_GROUP = LDAP_REQUIRE_GROUP + LDAP_DENY_GROUP = env("LDAP_DENY_GROUP", default="") + if LDAP_DENY_GROUP: + AUTH_LDAP_DENY_GROUP = LDAP_DENY_GROUP + + # SLUGLIFIER AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify" @@ -381,7 +444,7 @@ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS", "funkwhale_api.common.authentication.BearerTokenHeaderAuth", - "rest_framework_jwt.authentication.JSONWebTokenAuthentication", + "funkwhale_api.common.authentication.JSONWebTokenAuthentication", "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.BasicAuthentication", ), @@ -422,6 +485,11 @@ PROTECT_FILES_PATH = env("PROTECT_FILES_PATH", default="/_protected") # musicbrainz results. (value is in seconds) MUSICBRAINZ_CACHE_DURATION = env.int("MUSICBRAINZ_CACHE_DURATION", default=300) +# Use this setting to change the musicbrainz hostname, for instance to +# use a mirror. The hostname can also contain a port number (so, e.g., +# "localhost:5000" is a valid name to set). +MUSICBRAINZ_HOSTNAME = env("MUSICBRAINZ_HOSTNAME", default="musicbrainz.org") + # Custom Admin URL, use {% url 'admin:index' %} ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/") CSRF_USE_SESSIONS = True @@ -445,8 +513,14 @@ ACCOUNT_USERNAME_BLACKLIST = [ "me", "ghost", "_", + "-", "hello", "contact", + "inbox", + "outbox", + "shared-inbox", + "shared_inbox", + "actor", ] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[]) EXTERNAL_REQUESTS_VERIFY_SSL = env.bool("EXTERNAL_REQUESTS_VERIFY_SSL", default=True) diff --git a/api/config/settings/local.py b/api/config/settings/local.py index b8df4bdb7..f639fabd8 100644 --- a/api/config/settings/local.py +++ b/api/config/settings/local.py @@ -67,6 +67,7 @@ LOGGING = { "propagate": True, "level": "DEBUG", }, + "django_auth_ldap": {"handlers": ["console"], "level": "DEBUG"}, "": {"level": "DEBUG", "handlers": ["console"]}, }, } diff --git a/api/config/urls.py b/api/config/urls.py index 5ffcf211b..99fc32f1f 100644 --- a/api/config/urls.py +++ b/api/config/urls.py @@ -2,11 +2,13 @@ from __future__ import unicode_literals from django.conf import settings -from django.conf.urls import include, url +from django.conf.urls import url +from django.urls import include, path from django.conf.urls.static import static -from django.contrib import admin +from funkwhale_api.common import admin from django.views import defaults as default_views + urlpatterns = [ # Django Admin, use {% url 'admin:index' %} url(settings.ADMIN_URL, admin.site.urls), @@ -36,4 +38,6 @@ if settings.DEBUG: if "debug_toolbar" in settings.INSTALLED_APPS: import debug_toolbar - urlpatterns += [url(r"^__debug__/", include(debug_toolbar.urls))] + urlpatterns = [ + path("api/__debug__/", include(debug_toolbar.urls)) + ] + urlpatterns diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py index 5319b3c32..7ad37d53d 100644 --- a/api/funkwhale_api/__init__.py +++ b/api/funkwhale_api/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "0.16.3" +__version__ = "0.17" __version_info__ = tuple( [ int(num) if num.isdigit() else num diff --git a/api/funkwhale_api/common/admin.py b/api/funkwhale_api/common/admin.py new file mode 100644 index 000000000..4124a69b8 --- /dev/null +++ b/api/funkwhale_api/common/admin.py @@ -0,0 +1,19 @@ +from django.contrib.admin import register as initial_register, site, ModelAdmin # noqa +from django.db.models.fields.related import RelatedField + + +def register(model): + """ + To make the admin more performant, we ensure all the the relations + are listed under raw_id_fields + """ + + def decorator(modeladmin): + raw_id_fields = [] + for field in model._meta.fields: + if isinstance(field, RelatedField): + raw_id_fields.append(field.name) + setattr(modeladmin, "raw_id_fields", raw_id_fields) + return initial_register(model)(modeladmin) + + return decorator diff --git a/api/funkwhale_api/common/authentication.py b/api/funkwhale_api/common/authentication.py index 10bf36613..415b84cb2 100644 --- a/api/funkwhale_api/common/authentication.py +++ b/api/funkwhale_api/common/authentication.py @@ -56,3 +56,20 @@ class BearerTokenHeaderAuth(authentication.BaseJSONWebTokenAuthentication): def authenticate_header(self, request): return '{0} realm="{1}"'.format("Bearer", self.www_authenticate_realm) + + def authenticate(self, request): + auth = super().authenticate(request) + if auth: + if not auth[0].actor: + auth[0].create_actor() + return auth + + +class JSONWebTokenAuthentication(authentication.JSONWebTokenAuthentication): + def authenticate(self, request): + auth = super().authenticate(request) + + if auth: + if not auth[0].actor: + auth[0].create_actor() + return auth diff --git a/api/funkwhale_api/common/channels.py b/api/funkwhale_api/common/channels.py index a009ab5ab..b8106bef4 100644 --- a/api/funkwhale_api/common/channels.py +++ b/api/funkwhale_api/common/channels.py @@ -1,6 +1,25 @@ +import json +import logging + from asgiref.sync import async_to_sync from channels.layers import get_channel_layer +from django.core.serializers.json import DjangoJSONEncoder +logger = logging.getLogger(__file__) channel_layer = get_channel_layer() -group_send = async_to_sync(channel_layer.group_send) group_add = async_to_sync(channel_layer.group_add) + + +def group_send(group, event): + # we serialize the payload ourselves and deserialize it to ensure it + # works with msgpack. This is dirty, but we'll find a better solution + # later + s = json.dumps(event, cls=DjangoJSONEncoder) + event = json.loads(s) + logger.debug( + "[channels] Dispatching %s to group %s: %s", + event["type"], + group, + {"type": event["data"]["type"]}, + ) + async_to_sync(channel_layer.group_send)(group, event) diff --git a/api/funkwhale_api/common/consumers.py b/api/funkwhale_api/common/consumers.py index 47a666f05..48c318638 100644 --- a/api/funkwhale_api/common/consumers.py +++ b/api/funkwhale_api/common/consumers.py @@ -16,3 +16,5 @@ class JsonAuthConsumer(JsonWebsocketConsumer): super().accept() for group in self.groups: channels.group_add(group, self.channel_name) + for group in self.scope["user"].get_channels_groups(): + channels.group_add(group, self.channel_name) diff --git a/api/funkwhale_api/common/permissions.py b/api/funkwhale_api/common/permissions.py index 8f391a70c..4ab405eb4 100644 --- a/api/funkwhale_api/common/permissions.py +++ b/api/funkwhale_api/common/permissions.py @@ -9,7 +9,9 @@ from funkwhale_api.common import preferences class ConditionalAuthentication(BasePermission): def has_permission(self, request, view): if preferences.get("common__api_authentication_required"): - return request.user and request.user.is_authenticated + return (request.user and request.user.is_authenticated) or ( + hasattr(request, "actor") and request.actor + ) return True diff --git a/api/funkwhale_api/common/scripts/__init__.py b/api/funkwhale_api/common/scripts/__init__.py index 769fd00e4..c1fd39e61 100644 --- a/api/funkwhale_api/common/scripts/__init__.py +++ b/api/funkwhale_api/common/scripts/__init__.py @@ -1,6 +1,7 @@ from . import create_actors from . import create_image_variations from . import django_permissions_to_user_permissions +from . import migrate_to_user_libraries from . import test @@ -8,5 +9,6 @@ __all__ = [ "create_actors", "create_image_variations", "django_permissions_to_user_permissions", + "migrate_to_user_libraries", "test", ] diff --git a/api/funkwhale_api/common/scripts/migrate_to_user_libraries.py b/api/funkwhale_api/common/scripts/migrate_to_user_libraries.py new file mode 100644 index 000000000..537794b62 --- /dev/null +++ b/api/funkwhale_api/common/scripts/migrate_to_user_libraries.py @@ -0,0 +1,163 @@ +""" +Mirate instance files to a library #463. For each user that imported music on an +instance, we will create a "default" library with related files and an instance-level +visibility (unless instance has common__api_authentication_required set to False, +in which case the libraries will be public). + +Files without any import job will be bounded to a "default" library on the first +superuser account found. This should now happen though. + +This command will also generate federation ids for existing resources. + +""" + +from django.conf import settings +from django.db.models import functions, CharField, F, Value + +from funkwhale_api.music import models +from funkwhale_api.users.models import User +from funkwhale_api.federation import models as federation_models +from funkwhale_api.common import preferences + + +def create_libraries(open_api, stdout): + local_actors = federation_models.Actor.objects.exclude(user=None).only("pk", "user") + privacy_level = "everyone" if open_api else "instance" + stdout.write( + "* Creating {} libraries with {} visibility".format( + len(local_actors), privacy_level + ) + ) + libraries_by_user = {} + + for a in local_actors: + library, created = models.Library.objects.get_or_create( + name="default", actor=a, defaults={"privacy_level": privacy_level} + ) + libraries_by_user[library.actor.user.pk] = library.pk + if created: + stdout.write( + " * Created library {} for user {}".format(library.pk, a.user.pk) + ) + else: + stdout.write( + " * Found existing library {} for user {}".format( + library.pk, a.user.pk + ) + ) + + return libraries_by_user + + +def update_uploads(libraries_by_user, stdout): + stdout.write("* Updating uploads with proper libraries...") + for user_id, library_id in libraries_by_user.items(): + jobs = models.ImportJob.objects.filter( + upload__library=None, batch__submitted_by=user_id + ) + candidates = models.Upload.objects.filter( + pk__in=jobs.values_list("upload", flat=True) + ) + total = candidates.update(library=library_id, import_status="finished") + if total: + stdout.write( + " * Assigned {} uploads to user {}'s library".format(total, user_id) + ) + else: + stdout.write( + " * No uploads to assign to user {}'s library".format(user_id) + ) + + +def update_orphan_uploads(open_api, stdout): + privacy_level = "everyone" if open_api else "instance" + first_superuser = ( + User.objects.filter(is_superuser=True) + .exclude(actor=None) + .order_by("pk") + .first() + ) + if not first_superuser: + stdout.write("* No superuser found, skipping update orphan uploads") + return + library, _ = models.Library.objects.get_or_create( + name="default", + actor=first_superuser.actor, + defaults={"privacy_level": privacy_level}, + ) + candidates = ( + models.Upload.objects.filter(library=None, jobs__isnull=True) + .exclude(audio_file=None) + .exclude(audio_file="") + ) + + total = candidates.update(library=library, import_status="finished") + if total: + stdout.write( + "* Assigned {} orphaned uploads to superuser {}".format( + total, first_superuser.pk + ) + ) + else: + stdout.write("* No orphaned uploads found") + + +def set_fid(queryset, path, stdout): + model = queryset.model._meta.label + qs = queryset.filter(fid=None) + base_url = "{}{}".format(settings.FUNKWHALE_URL, path) + stdout.write( + "* Assigning federation ids to {} entries (path: {})".format(model, base_url) + ) + new_fid = functions.Concat(Value(base_url), F("uuid"), output_field=CharField()) + total = qs.update(fid=new_fid) + + stdout.write(" * {} entries updated".format(total)) + + +def update_shared_inbox_url(stdout): + stdout.write("* Update shared inbox url for local actors...") + candidates = federation_models.Actor.objects.local() + url = federation_models.get_shared_inbox_url() + candidates.update(shared_inbox_url=url) + + +def generate_actor_urls(part, stdout): + field = "{}_url".format(part) + stdout.write("* Update {} for local actors...".format(field)) + + queryset = federation_models.Actor.objects.local().filter(**{field: None}) + base_url = "{}/federation/actors/".format(settings.FUNKWHALE_URL) + + new_field = functions.Concat( + Value(base_url), + F("preferred_username"), + Value("/{}".format(part)), + output_field=CharField(), + ) + + queryset.update(**{field: new_field}) + + +def main(command, **kwargs): + open_api = not preferences.get("common__api_authentication_required") + libraries_by_user = create_libraries(open_api, command.stdout) + update_uploads(libraries_by_user, command.stdout) + update_orphan_uploads(open_api, command.stdout) + + set_fid_params = [ + ( + models.Upload.objects.exclude(library__actor__user=None), + "/federation/music/uploads/", + ), + (models.Artist.objects.all(), "/federation/music/artists/"), + (models.Album.objects.all(), "/federation/music/albums/"), + (models.Track.objects.all(), "/federation/music/tracks/"), + ] + for qs, path in set_fid_params: + set_fid(qs, path, command.stdout) + + update_shared_inbox_url(command.stdout) + + for part in ["followers", "following"]: + generate_actor_urls(part, command.stdout) diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py index 161c58102..c94b08d00 100644 --- a/api/funkwhale_api/common/serializers.py +++ b/api/funkwhale_api/common/serializers.py @@ -1,5 +1,70 @@ +import collections + from rest_framework import serializers +from django.core.exceptions import ObjectDoesNotExist +from django.utils.encoding import smart_text +from django.utils.translation import ugettext_lazy as _ + + +class RelatedField(serializers.RelatedField): + default_error_messages = { + "does_not_exist": _("Object with {related_field_name}={value} does not exist."), + "invalid": _("Invalid value."), + } + + def __init__(self, related_field_name, serializer, **kwargs): + self.related_field_name = related_field_name + self.serializer = serializer + self.filters = kwargs.pop("filters", None) + kwargs["queryset"] = kwargs.pop( + "queryset", self.serializer.Meta.model.objects.all() + ) + super().__init__(**kwargs) + + def get_filters(self, data): + filters = {self.related_field_name: data} + if self.filters: + filters.update(self.filters(self.context)) + return filters + + def to_internal_value(self, data): + try: + queryset = self.get_queryset() + filters = self.get_filters(data) + return queryset.get(**filters) + except ObjectDoesNotExist: + self.fail( + "does_not_exist", + related_field_name=self.related_field_name, + value=smart_text(data), + ) + except (TypeError, ValueError): + self.fail("invalid") + + def to_representation(self, obj): + return self.serializer.to_representation(obj) + + def get_choices(self, cutoff=None): + queryset = self.get_queryset() + if queryset is None: + # Ensure that field.choices returns something sensible + # even when accessed with a read-only field. + return {} + + if cutoff is not None: + queryset = queryset[:cutoff] + + return collections.OrderedDict( + [ + ( + self.to_representation(item)[self.related_field_name], + self.display_value(item), + ) + for item in queryset + ] + ) + class Action(object): def __init__(self, name, allow_all=False, qs_filter=None): @@ -21,6 +86,7 @@ class ActionSerializer(serializers.Serializer): objects = serializers.JSONField(required=True) filters = serializers.DictField(required=False) actions = None + pk_field = "pk" def __init__(self, *args, **kwargs): self.actions_by_name = {a.name: a for a in self.actions} @@ -51,7 +117,9 @@ class ActionSerializer(serializers.Serializer): 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( + **{"{}__in".format(self.pk_field): value} + ).order_by("id") raise serializers.ValidationError( "{} is not a valid value for objects. You must provide either a " diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py index bba4702b0..deda2f590 100644 --- a/api/funkwhale_api/common/utils.py +++ b/api/funkwhale_api/common/utils.py @@ -64,3 +64,46 @@ class ChunkedPath(object): new_filename = "".join(chunks[3:]) + ".{}".format(ext) parts = chunks[:3] + [new_filename] return os.path.join(self.root, *parts) + + +def chunk_queryset(source_qs, chunk_size): + """ + From https://github.com/peopledoc/django-chunkator/blob/master/chunkator/__init__.py + """ + pk = None + # In django 1.9, _fields is always present and `None` if 'values()' is used + # In Django 1.8 and below, _fields will only be present if using `values()` + has_fields = hasattr(source_qs, "_fields") and source_qs._fields + if has_fields: + if "pk" not in source_qs._fields: + raise ValueError("The values() call must include the `pk` field") + + field = source_qs.model._meta.pk + # set the correct field name: + # for ForeignKeys, we want to use `model_id` field, and not `model`, + # to bypass default ordering on related model + order_by_field = field.attname + + source_qs = source_qs.order_by(order_by_field) + queryset = source_qs + while True: + if pk: + queryset = source_qs.filter(pk__gt=pk) + page = queryset[:chunk_size] + page = list(page) + nb_items = len(page) + + if nb_items == 0: + return + + last_item = page[-1] + # source_qs._fields exists *and* is not none when using "values()" + if has_fields: + pk = last_item["pk"] + else: + pk = last_item.pk + + yield page + + if nb_items < chunk_size: + return diff --git a/api/funkwhale_api/favorites/admin.py b/api/funkwhale_api/favorites/admin.py index f56980e8c..05530b0c6 100644 --- a/api/funkwhale_api/favorites/admin.py +++ b/api/funkwhale_api/favorites/admin.py @@ -1,4 +1,4 @@ -from django.contrib import admin +from funkwhale_api.common import admin from . import models diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py index 16171aa34..66e10a1b4 100644 --- a/api/funkwhale_api/favorites/serializers.py +++ b/api/funkwhale_api/favorites/serializers.py @@ -1,4 +1,3 @@ - from rest_framework import serializers from funkwhale_api.activity import serializers as activity_serializers diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index dae90ebbd..7e30c28a6 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -3,9 +3,12 @@ from rest_framework.decorators import list_route from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response +from django.db.models import Prefetch + from funkwhale_api.activity import record from funkwhale_api.common import fields, permissions from funkwhale_api.music.models import Track +from funkwhale_api.music import utils as music_utils from . import filters, models, serializers @@ -19,11 +22,7 @@ class TrackFavoriteViewSet( filter_class = filters.TrackFavoriteFilter serializer_class = serializers.UserTrackFavoriteSerializer - queryset = ( - models.TrackFavorite.objects.all() - .select_related("track__artist", "track__album__artist", "user") - .prefetch_related("track__files") - ) + queryset = models.TrackFavorite.objects.all().select_related("user") permission_classes = [ permissions.ConditionalAuthentication, permissions.OwnerPermission, @@ -49,9 +48,14 @@ class TrackFavoriteViewSet( def get_queryset(self): queryset = super().get_queryset() - return queryset.filter( + queryset = queryset.filter( fields.privacy_level_query(self.request.user, "user__privacy_level") ) + tracks = Track.objects.annotate_playable_by_actor( + music_utils.get_actor_from_request(self.request) + ).select_related("artist", "album__artist") + queryset = queryset.prefetch_related(Prefetch("track", queryset=tracks)) + return queryset def perform_create(self, serializer): track = Track.objects.get(pk=serializer.data["track"]) diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index 73e83e334..498c76a99 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -1,3 +1,16 @@ +import uuid +import logging + +from django.db import transaction, IntegrityError +from django.db.models import Q + +from funkwhale_api.common import channels +from funkwhale_api.common import utils as funkwhale_utils + + +logger = logging.getLogger(__name__) +PUBLIC_ADDRESS = "https://www.w3.org/ns/activitystreams#Public" + ACTIVITY_TYPES = [ "Accept", "Add", @@ -48,14 +61,340 @@ OBJECT_TYPES = [ ] + ACTIVITY_TYPES -def deliver(activity, on_behalf_of, to=[]): +BROADCAST_TO_USER_ACTIVITIES = ["Follow", "Accept"] + + +@transaction.atomic +def receive(activity, on_behalf_of): + from . import models + from . import serializers from . import tasks - return tasks.send.delay(activity=activity, actor_id=on_behalf_of.pk, to=to) + # we ensure the activity has the bare minimum structure before storing + # it in our database + serializer = serializers.BaseActivitySerializer( + data=activity, context={"actor": on_behalf_of, "local_recipients": True} + ) + serializer.is_valid(raise_exception=True) + try: + copy = serializer.save() + except IntegrityError: + logger.warning( + "[federation] Discarding already elivered activity %s", + serializer.validated_data.get("id"), + ) + return + + local_to_recipients = get_actors_from_audience(activity.get("to", [])) + local_to_recipients = local_to_recipients.exclude(user=None) + + local_cc_recipients = get_actors_from_audience(activity.get("cc", [])) + local_cc_recipients = local_cc_recipients.exclude(user=None) + + inbox_items = [] + for recipients, type in [(local_to_recipients, "to"), (local_cc_recipients, "cc")]: + + for r in recipients.values_list("pk", flat=True): + inbox_items.append(models.InboxItem(actor_id=r, type=type, activity=copy)) + + models.InboxItem.objects.bulk_create(inbox_items) + + # at this point, we have the activity in database. Even if we crash, it's + # okay, as we can retry later + funkwhale_utils.on_commit(tasks.dispatch_inbox.delay, activity_id=copy.pk) + return copy -def accept_follow(follow): - from . import serializers +class Router: + def __init__(self): + self.routes = [] - serializer = serializers.AcceptFollowSerializer(follow) - return deliver(serializer.data, to=[follow.actor.url], on_behalf_of=follow.target) + def connect(self, route, handler): + self.routes.append((route, handler)) + + def register(self, route): + def decorator(handler): + self.connect(route, handler) + return handler + + return decorator + + +class InboxRouter(Router): + @transaction.atomic + def dispatch(self, payload, context): + """ + Receives an Activity payload and some context and trigger our + business logic + """ + from . import api_serializers + from . import models + + for route, handler in self.routes: + if match_route(route, payload): + r = handler(payload, context=context) + activity_obj = context.get("activity") + if activity_obj and r: + # handler returned additional data we can use + # to update the activity target + for key, value in r.items(): + setattr(activity_obj, key, value) + + update_fields = [] + for k in r.keys(): + if k in ["object", "target", "related_object"]: + update_fields += [ + "{}_id".format(k), + "{}_content_type".format(k), + ] + else: + update_fields.append(k) + activity_obj.save(update_fields=update_fields) + + if payload["type"] not in BROADCAST_TO_USER_ACTIVITIES: + return + + inbox_items = context.get( + "inbox_items", models.InboxItem.objects.none() + ) + inbox_items = ( + inbox_items.select_related() + .select_related("actor__user") + .prefetch_related( + "activity__object", + "activity__target", + "activity__related_object", + ) + ) + + for ii in inbox_items: + user = ii.actor.get_user() + if not user: + continue + group = "user.{}.inbox".format(user.pk) + channels.group_send( + group, + { + "type": "event.send", + "text": "", + "data": { + "type": "inbox.item_added", + "item": api_serializers.InboxItemSerializer(ii).data, + }, + }, + ) + return + + +class OutboxRouter(Router): + @transaction.atomic + def dispatch(self, routing, context): + """ + Receives a routing payload and some business objects in the context + and may yield data that should be persisted in the Activity model + for further delivery. + """ + from . import models + from . import tasks + + for route, handler in self.routes: + if not match_route(route, routing): + continue + + activities_data = [] + for e in handler(context): + # a route can yield zero, one or more activity payloads + if e: + activities_data.append(e) + inbox_items_by_activity_uuid = {} + deliveries_by_activity_uuid = {} + prepared_activities = [] + for activity_data in activities_data: + activity_data["payload"]["actor"] = activity_data["actor"].fid + to = activity_data["payload"].pop("to", []) + cc = activity_data["payload"].pop("cc", []) + a = models.Activity(**activity_data) + a.uuid = uuid.uuid4() + to_inbox_items, to_deliveries, new_to = prepare_deliveries_and_inbox_items( + to, "to" + ) + cc_inbox_items, cc_deliveries, new_cc = prepare_deliveries_and_inbox_items( + cc, "cc" + ) + if not any( + [to_inbox_items, to_deliveries, cc_inbox_items, cc_deliveries] + ): + continue + deliveries_by_activity_uuid[str(a.uuid)] = to_deliveries + cc_deliveries + inbox_items_by_activity_uuid[str(a.uuid)] = ( + to_inbox_items + cc_inbox_items + ) + if new_to: + a.payload["to"] = new_to + if new_cc: + a.payload["cc"] = new_cc + prepared_activities.append(a) + + activities = models.Activity.objects.bulk_create(prepared_activities) + + for activity in activities: + if str(activity.uuid) in deliveries_by_activity_uuid: + for obj in deliveries_by_activity_uuid[str(a.uuid)]: + obj.activity = activity + + if str(activity.uuid) in inbox_items_by_activity_uuid: + for obj in inbox_items_by_activity_uuid[str(a.uuid)]: + obj.activity = activity + + # create all deliveries and items, in bulk + models.Delivery.objects.bulk_create( + [ + obj + for collection in deliveries_by_activity_uuid.values() + for obj in collection + ] + ) + models.InboxItem.objects.bulk_create( + [ + obj + for collection in inbox_items_by_activity_uuid.values() + for obj in collection + ] + ) + + for a in activities: + funkwhale_utils.on_commit(tasks.dispatch_outbox.delay, activity_id=a.pk) + return activities + + +def recursive_gettattr(obj, key): + """ + Given a dictionary such as {'user': {'name': 'Bob'}} and + a dotted string such as user.name, returns 'Bob'. + + If the value is not present, returns None + """ + v = obj + for k in key.split("."): + v = v.get(k) + if v is None: + return + + return v + + +def match_route(route, payload): + for key, value in route.items(): + payload_value = recursive_gettattr(payload, key) + if payload_value != value: + return False + + return True + + +def prepare_deliveries_and_inbox_items(recipient_list, type): + """ + Given a list of recipients ( + either actor instances, public adresses, a dictionnary with a "type" and "target" + keys for followers collections) + returns a list of deliveries, alist of inbox_items and a list + of urls to persist in the activity in place of the initial recipient list. + """ + from . import models + + local_recipients = set() + remote_inbox_urls = set() + urls = [] + + for r in recipient_list: + if isinstance(r, models.Actor): + if r.is_local: + local_recipients.add(r) + else: + remote_inbox_urls.add(r.shared_inbox_url or r.inbox_url) + urls.append(r.fid) + elif r == PUBLIC_ADDRESS: + urls.append(r) + elif isinstance(r, dict) and r["type"] == "followers": + received_follows = ( + r["target"] + .received_follows.filter(approved=True) + .select_related("actor__user") + ) + for follow in received_follows: + actor = follow.actor + if actor.is_local: + local_recipients.add(actor) + else: + remote_inbox_urls.add(actor.shared_inbox_url or actor.inbox_url) + urls.append(r["target"].followers_url) + + deliveries = [models.Delivery(inbox_url=url) for url in remote_inbox_urls] + inbox_items = [ + models.InboxItem(actor=actor, type=type) for actor in local_recipients + ] + + return inbox_items, deliveries, urls + + +def join_queries_or(left, right): + if left: + return left | right + else: + return right + + +def get_actors_from_audience(urls): + """ + Given a list of urls such as [ + "https://hello.world/@bob/followers", + "https://eldritch.cafe/@alice/followers", + "https://funkwhale.demo/libraries/uuid/followers", + ] + Returns a queryset of actors that are member of the collections + listed in the given urls. The urls may contain urls referring + to an actor, an actor followers collection or an library followers + collection. + + Urls that don't match anything are simply discarded + """ + from . import models + + queries = {"followed": None, "actors": []} + for url in urls: + if url == PUBLIC_ADDRESS: + continue + queries["actors"].append(url) + queries["followed"] = join_queries_or( + queries["followed"], Q(target__followers_url=url) + ) + final_query = None + if queries["actors"]: + final_query = join_queries_or(final_query, Q(fid__in=queries["actors"])) + if queries["followed"]: + actor_follows = models.Follow.objects.filter(queries["followed"], approved=True) + final_query = join_queries_or( + final_query, Q(pk__in=actor_follows.values_list("actor", flat=True)) + ) + + library_follows = models.LibraryFollow.objects.filter( + queries["followed"], approved=True + ) + final_query = join_queries_or( + final_query, Q(pk__in=library_follows.values_list("actor", flat=True)) + ) + if not final_query: + return models.Actor.objects.none() + return models.Actor.objects.filter(final_query) + + +def get_inbox_urls(actor_queryset): + """ + Given an actor queryset, returns a deduplicated set containing + all inbox or shared inbox urls where we should deliver our payloads for + those actors + """ + values = actor_queryset.values("inbox_url", "shared_inbox_url") + + urls = set([actor["shared_inbox_url"] or actor["inbox_url"] for actor in values]) + return sorted(urls) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 7fbf815dc..f1b94809d 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -1,30 +1,16 @@ import datetime import logging -import xml from django.conf import settings -from django.db import transaction -from django.urls import reverse from django.utils import timezone -from rest_framework.exceptions import PermissionDenied from funkwhale_api.common import preferences, session -from funkwhale_api.common import utils as funkwhale_utils -from funkwhale_api.music import models as music_models -from funkwhale_api.music import tasks as music_tasks -from . import activity, keys, models, serializers, signing, utils +from . import models, serializers logger = logging.getLogger(__name__) -def remove_tags(text): - logger.debug("Removing tags from %s", text) - return "".join( - xml.etree.ElementTree.fromstring("
{}
".format(text)).itertext() - ) - - def get_actor_data(actor_url): response = session.get_session().get( actor_url, @@ -39,9 +25,9 @@ def get_actor_data(actor_url): raise ValueError("Invalid actor payload: {}".format(response.text)) -def get_actor(actor_url): +def get_actor(fid): try: - actor = models.Actor.objects.get(url=actor_url) + actor = models.Actor.objects.get(fid=fid) except models.Actor.DoesNotExist: actor = None fetch_delta = datetime.timedelta( @@ -50,330 +36,8 @@ def get_actor(actor_url): if actor and actor.last_fetch_date > timezone.now() - fetch_delta: # cache is hot, we can return as is return actor - data = get_actor_data(actor_url) + data = get_actor_data(fid) serializer = serializers.ActorSerializer(data=data) serializer.is_valid(raise_exception=True) return serializer.save(last_fetch_date=timezone.now()) - - -class SystemActor(object): - additional_attributes = {} - manually_approves_followers = False - - def get_request_auth(self): - actor = self.get_actor_instance() - return signing.get_auth(actor.private_key, actor.private_key_id) - - def serialize(self): - actor = self.get_actor_instance() - serializer = serializers.ActorSerializer(actor) - return serializer.data - - def get_actor_instance(self): - try: - return models.Actor.objects.get(url=self.get_actor_url()) - except models.Actor.DoesNotExist: - pass - private, public = keys.get_key_pair() - args = self.get_instance_argument( - 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") - return models.Actor.objects.create(**args) - - def get_actor_url(self): - return utils.full_url( - 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), - } - p.update(kwargs) - return p - - def get_inbox(self, data, actor=None): - raise NotImplementedError - - def post_inbox(self, data, actor=None): - return self.handle(data, actor=actor) - - def get_outbox(self, data, actor=None): - raise NotImplementedError - - def post_outbox(self, data, actor=None): - raise NotImplementedError - - def handle(self, data, actor=None): - """ - Main entrypoint for handling activities posted to the - actor's inbox - """ - logger.info("Received activity on %s inbox", self.id) - - if actor is None: - raise PermissionDenied("Actor not authenticated") - - 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())) - except (KeyError, AttributeError): - logger.debug("No handler for activity %s", ac["type"]) - return - - return handler(data, actor) - - def handle_follow(self, ac, sender): - serializer = serializers.FollowSerializer( - data=ac, context={"follow_actor": sender} - ) - if not serializer.is_valid(): - return logger.info("Invalid follow payload") - approved = True if not self.manually_approves_followers else None - follow = serializer.save(approved=approved) - if follow.approved: - return activity.accept_follow(follow) - - 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} - ) - if not serializer.is_valid(raise_exception=True): - 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} - ) - if not serializer.is_valid(): - return logger.info("Received invalid payload") - serializer.save() - - def handle_undo(self, ac, sender): - if ac["object"]["type"] != "Follow": - return - - if ac["object"]["actor"] != sender.url: - # not the same actor, permission issue - return - - self.handle_undo_follow(ac, sender) - - -class LibraryActor(SystemActor): - 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")), - } - ) - return data - - @property - def manually_approves_followers(self): - 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 - ) - except models.Library.DoesNotExist: - logger.info("Skipping import, we're not following %s", sender.url) - return - - if ac["object"]["type"] != "Collection": - return - - if ac["object"]["totalItems"] <= 0: - return - - try: - items = ac["object"]["items"] - except KeyError: - logger.warning("No items in collection!") - return - - item_serializers = [ - serializers.AudioSerializer(data=i, context={"library": remote_library}) - for i in items - ] - now = timezone.now() - valid_serializers = [] - for s in item_serializers: - if s.is_valid(): - valid_serializers.append(s) - else: - 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") - 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 - ) - funkwhale_utils.on_commit( - music_tasks.import_job_run.delay, - import_job_id=job.pk, - use_acoustid=False, - ) - - -class TestActor(SystemActor): - id = "test" - name = "{host}'s test account" - summary = ( - "Bot account to test federation with {host}. " - "Send me /ping and I'll answer you." - ) - additional_attributes = {"manually_approves_followers": False} - manually_approves_followers = False - - def get_outbox(self, data, actor=None): - return { - "@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}) - ), - "type": "OrderedCollection", - "totalItems": 0, - "orderedItems": [], - } - - def parse_command(self, message): - """ - Remove any links or fancy markup to extract /command from - a note message. - """ - raw = remove_tags(message) - try: - return raw.split("/")[1] - except IndexError: - return - - def handle_create(self, ac, sender): - 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": - return - - now = timezone.now() - test_actor = self.get_actor_instance() - reply_url = "https://{}/activities/note/{}".format( - settings.FEDERATION_HOSTNAME, now.timestamp() - ) - 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, - } - ], - }, - } - 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 - )[0] - activity.deliver( - serializers.FollowSerializer(follow_back).data, - to=[follow_back.target.url], - 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) - except models.Follow.DoesNotExist: - return - undo = serializers.UndoFollowSerializer(follow).data - follow.delete() - activity.deliver(undo, to=[sender.url], on_behalf_of=actor) - - -SYSTEM_ACTORS = {"library": LibraryActor(), "test": TestActor()} diff --git a/api/funkwhale_api/federation/admin.py b/api/funkwhale_api/federation/admin.py index a82e9aaf2..98bc65247 100644 --- a/api/funkwhale_api/federation/admin.py +++ b/api/funkwhale_api/federation/admin.py @@ -1,19 +1,49 @@ -from django.contrib import admin +from funkwhale_api.common import admin from . import models +from . import tasks + + +def redeliver_deliveries(modeladmin, request, queryset): + queryset.update(is_delivered=False) + for delivery in queryset: + tasks.deliver_to_remote.delay(delivery_id=delivery.pk) + + +redeliver_deliveries.short_description = "Redeliver" + + +def redeliver_activities(modeladmin, request, queryset): + for activity in queryset.select_related("actor__user"): + if activity.actor.get_user(): + tasks.dispatch_outbox.delay(activity_id=activity.pk) + else: + tasks.dispatch_inbox.delay(activity_id=activity.pk) + + +redeliver_activities.short_description = "Redeliver" + + +@admin.register(models.Activity) +class ActivityAdmin(admin.ModelAdmin): + list_display = ["type", "fid", "url", "actor", "creation_date"] + search_fields = ["payload", "fid", "url", "actor__domain"] + list_filter = ["type", "actor__domain"] + actions = [redeliver_activities] + list_select_related = True @admin.register(models.Actor) class ActorAdmin(admin.ModelAdmin): list_display = [ - "url", + "fid", "domain", "preferred_username", "type", "creation_date", "last_fetch_date", ] - search_fields = ["url", "domain", "preferred_username"] + search_fields = ["fid", "domain", "preferred_username"] list_filter = ["type"] @@ -21,28 +51,36 @@ class ActorAdmin(admin.ModelAdmin): class FollowAdmin(admin.ModelAdmin): list_display = ["actor", "target", "approved", "creation_date"] list_filter = ["approved"] - search_fields = ["actor__url", "target__url"] + search_fields = ["actor__fid", "target__fid"] 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"] +@admin.register(models.LibraryFollow) +class LibraryFollowAdmin(admin.ModelAdmin): + list_display = ["actor", "target", "approved", "creation_date"] + list_filter = ["approved"] + search_fields = ["actor__fid", "target__fid"] list_select_related = True -@admin.register(models.LibraryTrack) -class LibraryTrackAdmin(admin.ModelAdmin): +@admin.register(models.InboxItem) +class InboxItemAdmin(admin.ModelAdmin): + list_display = ["actor", "activity", "type", "is_read"] + list_filter = ["type", "activity__type", "is_read"] + search_fields = ["actor__fid", "activity__fid"] + list_select_related = True + + +@admin.register(models.Delivery) +class DeliveryAdmin(admin.ModelAdmin): list_display = [ - "title", - "artist_name", - "album_title", - "url", - "library", - "creation_date", - "published_date", + "inbox_url", + "activity", + "last_attempt_date", + "attempts", + "is_delivered", ] - search_fields = ["library__url", "url", "artist_name", "title", "album_title"] + list_filter = ["activity__type", "is_delivered"] + search_fields = ["inbox_url"] list_select_related = True + actions = [redeliver_deliveries] diff --git a/api/funkwhale_api/federation/api_serializers.py b/api/funkwhale_api/federation/api_serializers.py new file mode 100644 index 000000000..9041ed28a --- /dev/null +++ b/api/funkwhale_api/federation/api_serializers.py @@ -0,0 +1,146 @@ +from rest_framework import serializers + +from funkwhale_api.common import serializers as common_serializers +from funkwhale_api.music import models as music_models + +from . import filters +from . import models +from . import serializers as federation_serializers + + +class NestedLibraryFollowSerializer(serializers.ModelSerializer): + class Meta: + model = models.LibraryFollow + fields = ["creation_date", "uuid", "fid", "approved", "modification_date"] + + +class LibraryScanSerializer(serializers.ModelSerializer): + class Meta: + model = music_models.LibraryScan + fields = [ + "total_files", + "processed_files", + "errored_files", + "status", + "creation_date", + "modification_date", + ] + + +class LibrarySerializer(serializers.ModelSerializer): + actor = federation_serializers.APIActorSerializer() + uploads_count = serializers.SerializerMethodField() + latest_scan = serializers.SerializerMethodField() + follow = serializers.SerializerMethodField() + + class Meta: + model = music_models.Library + fields = [ + "fid", + "uuid", + "actor", + "name", + "description", + "creation_date", + "uploads_count", + "privacy_level", + "follow", + "latest_scan", + ] + + def get_uploads_count(self, o): + return max(getattr(o, "_uploads_count", 0), o.uploads_count) + + def get_follow(self, o): + try: + return NestedLibraryFollowSerializer(o._follows[0]).data + except (AttributeError, IndexError): + return None + + def get_latest_scan(self, o): + scan = o.scans.order_by("-creation_date").first() + if scan: + return LibraryScanSerializer(scan).data + + +class LibraryFollowSerializer(serializers.ModelSerializer): + target = common_serializers.RelatedField("uuid", LibrarySerializer(), required=True) + actor = serializers.SerializerMethodField() + + class Meta: + model = models.LibraryFollow + fields = ["creation_date", "actor", "uuid", "target", "approved"] + read_only_fields = ["uuid", "actor", "approved", "creation_date"] + + def validate_target(self, v): + actor = self.context["actor"] + if v.actor == actor: + raise serializers.ValidationError("You cannot follow your own library") + + if v.received_follows.filter(actor=actor).exists(): + raise serializers.ValidationError("You are already following this library") + return v + + def get_actor(self, o): + return federation_serializers.APIActorSerializer(o.actor).data + + +def serialize_generic_relation(activity, obj): + data = {"uuid": obj.uuid, "type": obj._meta.label} + if data["type"] == "music.Library": + data["name"] = obj.name + if data["type"] == "federation.LibraryFollow": + data["approved"] = obj.approved + + return data + + +class ActivitySerializer(serializers.ModelSerializer): + actor = federation_serializers.APIActorSerializer() + object = serializers.SerializerMethodField() + target = serializers.SerializerMethodField() + related_object = serializers.SerializerMethodField() + + class Meta: + model = models.Activity + fields = [ + "uuid", + "fid", + "actor", + "payload", + "object", + "target", + "related_object", + "actor", + "creation_date", + "type", + ] + + def get_object(self, o): + if o.object: + return serialize_generic_relation(o, o.object) + + def get_related_object(self, o): + if o.related_object: + return serialize_generic_relation(o, o.related_object) + + def get_target(self, o): + if o.target: + return serialize_generic_relation(o, o.target) + + +class InboxItemSerializer(serializers.ModelSerializer): + activity = ActivitySerializer() + + class Meta: + model = models.InboxItem + fields = ["id", "type", "activity", "is_read"] + read_only_fields = ["id", "type", "activity"] + + +class InboxItemActionSerializer(common_serializers.ActionSerializer): + actions = [common_serializers.Action("read", allow_all=True)] + filterset_class = filters.InboxItemFilter + + def handle_read(self, objects): + return objects.update(is_read=True) diff --git a/api/funkwhale_api/federation/api_urls.py b/api/funkwhale_api/federation/api_urls.py index 625043bf6..e1e451bff 100644 --- a/api/funkwhale_api/federation/api_urls.py +++ b/api/funkwhale_api/federation/api_urls.py @@ -1,9 +1,10 @@ from rest_framework import routers -from . import views +from . import api_views router = routers.SimpleRouter() -router.register(r"libraries", views.LibraryViewSet, "libraries") -router.register(r"library-tracks", views.LibraryTrackViewSet, "library-tracks") +router.register(r"follows/library", api_views.LibraryFollowViewSet, "library-follows") +router.register(r"inbox", api_views.InboxItemViewSet, "inbox") +router.register(r"libraries", api_views.LibraryViewSet, "libraries") urlpatterns = router.urls diff --git a/api/funkwhale_api/federation/api_views.py b/api/funkwhale_api/federation/api_views.py new file mode 100644 index 000000000..75ffad0b2 --- /dev/null +++ b/api/funkwhale_api/federation/api_views.py @@ -0,0 +1,180 @@ +import requests.exceptions + +from django.db import transaction +from django.db.models import Count + +from rest_framework import decorators +from rest_framework import mixins +from rest_framework import permissions +from rest_framework import response +from rest_framework import viewsets + +from funkwhale_api.music import models as music_models + +from . import activity +from . import api_serializers +from . import filters +from . import models +from . import routes +from . import serializers +from . import utils + + +@transaction.atomic +def update_follow(follow, approved): + follow.approved = approved + follow.save(update_fields=["approved"]) + routes.outbox.dispatch({"type": "Accept"}, context={"follow": follow}) + + +class LibraryFollowViewSet( + mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): + lookup_field = "uuid" + queryset = ( + models.LibraryFollow.objects.all() + .order_by("-creation_date") + .select_related("actor", "target__actor") + ) + serializer_class = api_serializers.LibraryFollowSerializer + permission_classes = [permissions.IsAuthenticated] + filter_class = filters.LibraryFollowFilter + ordering_fields = ("creation_date",) + + def get_queryset(self): + qs = super().get_queryset() + return qs.filter(actor=self.request.user.actor) + + def perform_create(self, serializer): + follow = serializer.save(actor=self.request.user.actor) + routes.outbox.dispatch({"type": "Follow"}, context={"follow": follow}) + + @transaction.atomic + def perform_destroy(self, instance): + routes.outbox.dispatch( + {"type": "Undo", "object": {"type": "Follow"}}, context={"follow": instance} + ) + instance.delete() + + def get_serializer_context(self): + context = super().get_serializer_context() + context["actor"] = self.request.user.actor + return context + + @decorators.detail_route(methods=["post"]) + def accept(self, request, *args, **kwargs): + try: + follow = self.queryset.get( + target__actor=self.request.user.actor, uuid=kwargs["uuid"] + ) + except models.LibraryFollow.DoesNotExist: + return response.Response({}, status=404) + update_follow(follow, approved=True) + return response.Response(status=204) + + @decorators.detail_route(methods=["post"]) + def reject(self, request, *args, **kwargs): + try: + follow = self.queryset.get( + target__actor=self.request.user.actor, uuid=kwargs["uuid"] + ) + except models.LibraryFollow.DoesNotExist: + return response.Response({}, status=404) + + update_follow(follow, approved=False) + return response.Response(status=204) + + +class LibraryViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + lookup_field = "uuid" + queryset = ( + music_models.Library.objects.all() + .order_by("-creation_date") + .select_related("actor__user") + .annotate(_uploads_count=Count("uploads")) + ) + serializer_class = api_serializers.LibrarySerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + qs = super().get_queryset() + return qs.viewable_by(actor=self.request.user.actor) + + @decorators.detail_route(methods=["post"]) + def scan(self, request, *args, **kwargs): + library = self.get_object() + if library.actor.get_user(): + return response.Response({"status": "skipped"}, 200) + + scan = library.schedule_scan(actor=request.user.actor) + if scan: + return response.Response( + { + "status": "scheduled", + "scan": api_serializers.LibraryScanSerializer(scan).data, + }, + 200, + ) + return response.Response({"status": "skipped"}, 200) + + @decorators.list_route(methods=["post"]) + def fetch(self, request, *args, **kwargs): + try: + fid = request.data["fid"] + except KeyError: + return response.Response({"fid": ["This field is required"]}) + try: + library = utils.retrieve( + fid, + queryset=self.queryset, + serializer_class=serializers.LibrarySerializer, + ) + except requests.exceptions.RequestException as e: + return response.Response( + {"detail": "Error while fetching the library: {}".format(str(e))}, + status=400, + ) + except serializers.serializers.ValidationError as e: + return response.Response( + {"detail": "Invalid data in remote library: {}".format(str(e))}, + status=400, + ) + serializer = self.serializer_class(library) + return response.Response({"count": 1, "results": [serializer.data]}) + + +class InboxItemViewSet( + mixins.UpdateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, +): + + queryset = ( + models.InboxItem.objects.select_related("activity__actor") + .prefetch_related("activity__object", "activity__target") + .filter(activity__type__in=activity.BROADCAST_TO_USER_ACTIVITIES, type="to") + .order_by("-activity__creation_date") + ) + serializer_class = api_serializers.InboxItemSerializer + permission_classes = [permissions.IsAuthenticated] + filter_class = filters.InboxItemFilter + ordering_fields = ("activity__creation_date",) + + def get_queryset(self): + qs = super().get_queryset() + return qs.filter(actor=self.request.user.actor) + + @decorators.list_route(methods=["post"]) + def action(self, request, *args, **kwargs): + queryset = self.get_queryset() + serializer = api_serializers.InboxItemActionSerializer( + request.data, queryset=queryset + ) + serializer.is_valid(raise_exception=True) + result = serializer.save() + return response.Response(result, status=200) diff --git a/api/funkwhale_api/federation/dynamic_preferences_registry.py b/api/funkwhale_api/federation/dynamic_preferences_registry.py index 5119d2596..01bfdf727 100644 --- a/api/funkwhale_api/federation/dynamic_preferences_registry.py +++ b/api/funkwhale_api/federation/dynamic_preferences_registry.py @@ -1,4 +1,3 @@ - from dynamic_preferences import types from dynamic_preferences.registries import global_preferences_registry diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 4a13842da..a52cf88be 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -8,6 +8,7 @@ from django.utils import timezone from django.utils.http import http_date from funkwhale_api.factories import registry +from funkwhale_api.users import factories as user_factories from . import keys, models @@ -61,6 +62,10 @@ class LinkFactory(factory.Factory): audio = factory.Trait(mediaType=factory.Iterator(["audio/mp3", "audio/ogg"])) +def create_user(actor): + return user_factories.UserFactory(actor=actor) + + @registry.register class ActorFactory(factory.DjangoModelFactory): public_key = None @@ -68,9 +73,12 @@ class ActorFactory(factory.DjangoModelFactory): preferred_username = factory.Faker("user_name") summary = factory.Faker("paragraph") domain = factory.Faker("domain_name") - url = factory.LazyAttribute( + fid = factory.LazyAttribute( lambda o: "https://{}/users/{}".format(o.domain, o.preferred_username) ) + followers_url = factory.LazyAttribute( + lambda o: "https://{}/users/{}followers".format(o.domain, o.preferred_username) + ) inbox_url = factory.LazyAttribute( lambda o: "https://{}/users/{}/inbox".format(o.domain, o.preferred_username) ) @@ -81,20 +89,34 @@ class ActorFactory(factory.DjangoModelFactory): class Meta: model = models.Actor - class Params: - local = factory.Trait( - domain=factory.LazyAttribute(lambda o: settings.FEDERATION_HOSTNAME) - ) + @factory.post_generation + def local(self, create, extracted, **kwargs): + if not extracted and not kwargs: + return + from funkwhale_api.users.factories import UserFactory - @classmethod - def _generate(cls, create, attrs): - 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: + self.domain = settings.FEDERATION_HOSTNAME + self.save(update_fields=["domain"]) + if not create: + if extracted and hasattr(extracted, "pk"): + extracted.actor = self + else: + UserFactory.build(actor=self, **kwargs) + if extracted and hasattr(extracted, "pk"): + extracted.actor = self + extracted.save(update_fields=["user"]) + else: + self.user = UserFactory(actor=self, **kwargs) + + @factory.post_generation + def keys(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + if not extracted: private, public = keys.get_key_pair() - attrs["private_key"] = private.decode("utf-8") - attrs["public_key"] = public.decode("utf-8") - return super()._generate(create, attrs) + self.private_key = private.decode("utf-8") + self.public_key = public.decode("utf-8") @registry.register @@ -110,15 +132,72 @@ class FollowFactory(factory.DjangoModelFactory): @registry.register -class LibraryFactory(factory.DjangoModelFactory): +class MusicLibraryFactory(factory.django.DjangoModelFactory): actor = factory.SubFactory(ActorFactory) - url = factory.Faker("url") - federation_enabled = True - download_files = False - autoimport = False + privacy_level = "me" + name = factory.Faker("sentence") + description = factory.Faker("sentence") + uploads_count = 0 + fid = factory.Faker("federation_url") class Meta: - model = models.Library + model = "music.Library" + + @factory.post_generation + def followers_url(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + self.followers_url = extracted or self.fid + "/followers" + + +@registry.register +class LibraryScan(factory.django.DjangoModelFactory): + library = factory.SubFactory(MusicLibraryFactory) + actor = factory.SubFactory(ActorFactory) + total_files = factory.LazyAttribute(lambda o: o.library.uploads_count) + + class Meta: + model = "music.LibraryScan" + + +@registry.register +class ActivityFactory(factory.django.DjangoModelFactory): + actor = factory.SubFactory(ActorFactory) + url = factory.Faker("federation_url") + payload = factory.LazyFunction(lambda: {"type": "Create"}) + + class Meta: + model = "federation.Activity" + + +@registry.register +class InboxItemFactory(factory.django.DjangoModelFactory): + actor = factory.SubFactory(ActorFactory, local=True) + activity = factory.SubFactory(ActivityFactory) + type = "to" + + class Meta: + model = "federation.InboxItem" + + +@registry.register +class DeliveryFactory(factory.django.DjangoModelFactory): + activity = factory.SubFactory(ActivityFactory) + inbox_url = factory.Faker("url") + + class Meta: + model = "federation.Delivery" + + +@registry.register +class LibraryFollowFactory(factory.DjangoModelFactory): + target = factory.SubFactory(MusicLibraryFactory) + actor = factory.SubFactory(ActorFactory) + + class Meta: + model = "federation.LibraryFollow" class ArtistMetadataFactory(factory.Factory): @@ -161,25 +240,6 @@ class LibraryTrackMetadataFactory(factory.Factory): model = dict -@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" - metadata = factory.SubFactory(LibraryTrackMetadataFactory) - published_date = factory.LazyFunction(timezone.now) - - class Meta: - model = models.LibraryTrack - - class Params: - with_audio_file = factory.Trait(audio_file=factory.django.FileField()) - - @registry.register(name="federation.Note") class NoteFactory(factory.Factory): type = "Note" @@ -192,22 +252,6 @@ class NoteFactory(factory.Factory): model = dict -@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") - object = factory.SubFactory( - NoteFactory, - actor=factory.SelfAttribute("..actor"), - published=factory.SelfAttribute("..published"), - ) - - class Meta: - model = dict - - @registry.register(name="federation.AudioMetadata") class AudioMetadataFactory(factory.Factory): recording = factory.LazyAttribute( @@ -230,9 +274,9 @@ class AudioMetadataFactory(factory.Factory): @registry.register(name="federation.Audio") class AudioFactory(factory.Factory): type = "Audio" - id = factory.Faker("url") + id = factory.Faker("federation_url") published = factory.LazyFunction(lambda: timezone.now().isoformat()) - actor = factory.Faker("url") + actor = factory.Faker("federation_url") url = factory.SubFactory(LinkFactory, audio=True) metadata = factory.SubFactory(LibraryTrackMetadataFactory) diff --git a/api/funkwhale_api/federation/filters.py b/api/funkwhale_api/federation/filters.py index ff7575ba5..3a8b76cee 100644 --- a/api/funkwhale_api/federation/filters.py +++ b/api/funkwhale_api/federation/filters.py @@ -1,68 +1,10 @@ -import django_filters +import django_filters.widgets from funkwhale_api.common import fields -from funkwhale_api.common import search from . import models -class LibraryFilter(django_filters.FilterSet): - 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"], - } - - -class LibraryTrackFilter(django_filters.FilterSet): - library = django_filters.CharFilter("library__uuid") - status = django_filters.CharFilter(method="filter_status") - q = fields.SmartSearchFilter( - config=search.SearchConfig( - search_fields={ - "domain": {"to": "library__actor__domain"}, - "artist": {"to": "artist_name"}, - "album": {"to": "album_title"}, - "title": {"to": "title"}, - }, - filter_fields={ - "domain": {"to": "library__actor__domain"}, - "artist": {"to": "artist_name__iexact"}, - "album": {"to": "album_title__iexact"}, - "title": {"to": "title__iexact"}, - }, - ) - ) - - def filter_status(self, queryset, field_name, value): - 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") - 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"], - } - - class FollowFilter(django_filters.FilterSet): pending = django_filters.CharFilter(method="filter_pending") ordering = django_filters.OrderingFilter( @@ -84,3 +26,23 @@ class FollowFilter(django_filters.FilterSet): if value.lower() in ["true", "1", "yes"]: queryset = queryset.filter(approved__isnull=True) return queryset + + +class LibraryFollowFilter(django_filters.FilterSet): + class Meta: + model = models.LibraryFollow + fields = ["approved"] + + +class InboxItemFilter(django_filters.FilterSet): + is_read = django_filters.BooleanFilter( + "is_read", widget=django_filters.widgets.BooleanWidget() + ) + before = django_filters.NumberFilter(method="filter_before") + + class Meta: + model = models.InboxItem + fields = ["is_read", "activity__type", "activity__actor"] + + def filter_before(self, queryset, field_name, value): + return queryset.filter(pk__lte=value) diff --git a/api/funkwhale_api/federation/library.py b/api/funkwhale_api/federation/library.py index d2ccb1952..e7f8373fa 100644 --- a/api/funkwhale_api/federation/library.py +++ b/api/funkwhale_api/federation/library.py @@ -1,78 +1,12 @@ -import json - import requests from django.conf import settings from funkwhale_api.common import session -from . import actors, models, serializers, signing, webfinger +from . import serializers, signing -def scan_from_account_name(account_name): - """ - Given an account name such as library@test.library, will: - - 1. Perform the webfinger lookup - 2. Perform the actor lookup - 3. Perform the library's collection lookup - - and return corresponding data in a dictionary. - """ - data = {} - try: - 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() - 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 - except models.Follow.DoesNotExist: - pass - - try: - data["webfinger"] = webfinger.get_resource("acct:{}".format(account_name)) - except requests.ConnectionError: - 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) - ] - } - } - except json.JSONDecodeError as e: - return {"webfinger": {"errors": ["Could not process webfinger response"]}} - - try: - data["actor"] = actors.get_actor_data(data["webfinger"]["actor_url"]) - except requests.ConnectionError: - 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)] - } - return data - - serializer = serializers.LibraryActorSerializer(data=data["actor"]) - if not serializer.is_valid(): - data["actor"] = {"errors": ["Invalid ActivityPub actor"]} - return data - 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() +def get_library_data(library_url, actor): auth = signing.get_auth(actor.private_key, actor.private_key_id) try: response = session.get_session().get( @@ -91,15 +25,14 @@ def get_library_data(library_url): 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()) + serializer = serializers.LibrarySerializer(data=response.json()) if not serializer.is_valid(): 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() +def get_library_page(library, page_url, actor): auth = signing.get_auth(actor.private_key, actor.private_key_id) response = session.get_session().get( page_url, @@ -110,7 +43,7 @@ def get_library_page(library, page_url): ) serializer = serializers.CollectionPageSerializer( data=response.json(), - context={"library": library, "item_serializer": serializers.AudioSerializer}, + context={"library": library, "item_serializer": serializers.UploadSerializer}, ) serializer.is_valid(raise_exception=True) return serializer.validated_data diff --git a/api/funkwhale_api/federation/migrations/0007_auto_20180807_1748.py b/api/funkwhale_api/federation/migrations/0007_auto_20180807_1748.py new file mode 100644 index 000000000..65bbc4cd8 --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0007_auto_20180807_1748.py @@ -0,0 +1,95 @@ +# Generated by Django 2.0.7 on 2018-08-07 17:48 + +import django.contrib.postgres.fields.jsonb +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [("federation", "0006_auto_20180521_1702")] + + operations = [ + migrations.CreateModel( + name="Activity", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("uuid", models.UUIDField(default=uuid.uuid4, unique=True)), + ( + "fid", + models.URLField(blank=True, max_length=500, null=True, unique=True), + ), + ("url", models.URLField(blank=True, max_length=500, null=True)), + ( + "payload", + django.contrib.postgres.fields.jsonb.JSONField( + default={}, + encoder=django.core.serializers.json.DjangoJSONEncoder, + max_length=50000, + ), + ), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("delivered", models.NullBooleanField(default=None)), + ("delivered_date", models.DateTimeField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name="LibraryFollow", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "fid", + models.URLField(blank=True, max_length=500, null=True, unique=True), + ), + ("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)), + ], + ), + migrations.RenameField("actor", "url", "fid"), + migrations.AddField( + model_name="actor", + name="url", + field=models.URLField(blank=True, max_length=500, null=True), + ), + migrations.AddField( + model_name="follow", + name="fid", + field=models.URLField(blank=True, max_length=500, null=True, unique=True), + ), + migrations.AddField( + model_name="libraryfollow", + name="actor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="library_follows", + to="federation.Actor", + ), + ), + ] diff --git a/api/funkwhale_api/federation/migrations/0008_auto_20180807_1748.py b/api/funkwhale_api/federation/migrations/0008_auto_20180807_1748.py new file mode 100644 index 000000000..f86a1e31c --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0008_auto_20180807_1748.py @@ -0,0 +1,36 @@ +# Generated by Django 2.0.7 on 2018-08-07 17:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("music", "0029_auto_20180807_1748"), + ("federation", "0007_auto_20180807_1748"), + ] + + operations = [ + migrations.AddField( + model_name="libraryfollow", + name="target", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="received_follows", + to="music.Library", + ), + ), + migrations.AddField( + model_name="activity", + name="actor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="activities", + to="federation.Actor", + ), + ), + migrations.AlterUniqueTogether( + name="libraryfollow", unique_together={("actor", "target")} + ), + ] diff --git a/api/funkwhale_api/federation/migrations/0009_auto_20180822_1956.py b/api/funkwhale_api/federation/migrations/0009_auto_20180822_1956.py new file mode 100644 index 000000000..042a86143 --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0009_auto_20180822_1956.py @@ -0,0 +1,44 @@ +# Generated by Django 2.0.8 on 2018-08-22 19:56 + +import django.contrib.postgres.fields.jsonb +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import funkwhale_api.federation.models + + +class Migration(migrations.Migration): + + dependencies = [("federation", "0008_auto_20180807_1748")] + + operations = [ + migrations.AddField( + model_name="activity", + name="recipient", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="inbox_activities", + to="federation.Actor", + ), + ), + migrations.AlterField( + model_name="activity", + name="payload", + field=django.contrib.postgres.fields.jsonb.JSONField( + default=funkwhale_api.federation.models.empty_dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + max_length=50000, + ), + ), + migrations.AlterField( + model_name="librarytrack", + name="metadata", + field=django.contrib.postgres.fields.jsonb.JSONField( + default=funkwhale_api.federation.models.empty_dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + max_length=10000, + ), + ), + ] diff --git a/api/funkwhale_api/federation/migrations/0010_auto_20180904_2011.py b/api/funkwhale_api/federation/migrations/0010_auto_20180904_2011.py new file mode 100644 index 000000000..62b4c73fb --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0010_auto_20180904_2011.py @@ -0,0 +1,74 @@ +# Generated by Django 2.0.8 on 2018-09-04 20:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [("federation", "0009_auto_20180822_1956")] + + operations = [ + migrations.CreateModel( + name="InboxItem", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("is_delivered", models.BooleanField(default=False)), + ( + "type", + models.CharField( + choices=[("to", "to"), ("cc", "cc")], max_length=10 + ), + ), + ("last_delivery_date", models.DateTimeField(blank=True, null=True)), + ("delivery_attempts", models.PositiveIntegerField(default=0)), + ], + ), + migrations.RemoveField(model_name="activity", name="delivered"), + migrations.RemoveField(model_name="activity", name="delivered_date"), + migrations.RemoveField(model_name="activity", name="recipient"), + migrations.AlterField( + model_name="activity", + name="actor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="outbox_activities", + to="federation.Actor", + ), + ), + migrations.AddField( + model_name="inboxitem", + name="activity", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="inbox_items", + to="federation.Activity", + ), + ), + migrations.AddField( + model_name="inboxitem", + name="actor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="inbox_items", + to="federation.Actor", + ), + ), + migrations.AddField( + model_name="activity", + name="recipients", + field=models.ManyToManyField( + related_name="inbox_activities", + through="federation.InboxItem", + to="federation.Actor", + ), + ), + ] diff --git a/api/funkwhale_api/federation/migrations/0011_auto_20180910_1902.py b/api/funkwhale_api/federation/migrations/0011_auto_20180910_1902.py new file mode 100644 index 000000000..feeeaff86 --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0011_auto_20180910_1902.py @@ -0,0 +1,61 @@ +# Generated by Django 2.0.8 on 2018-09-10 19:02 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('federation', '0010_auto_20180904_2011'), + ] + + operations = [ + migrations.AddField( + model_name='activity', + name='object_content_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objecting_activities', to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='activity', + name='object_id', + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name='activity', + name='related_object_content_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='related_objecting_activities', to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='activity', + name='related_object_id', + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name='activity', + name='target_content_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='targeting_activities', to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='activity', + name='target_id', + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name='activity', + name='type', + field=models.CharField(db_index=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='inboxitem', + name='is_read', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='activity', + name='creation_date', + field=models.DateTimeField(db_index=True, default=django.utils.timezone.now), + ), + ] diff --git a/api/funkwhale_api/federation/migrations/0012_auto_20180920_1803.py b/api/funkwhale_api/federation/migrations/0012_auto_20180920_1803.py new file mode 100644 index 000000000..3e44c2d9d --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0012_auto_20180920_1803.py @@ -0,0 +1,37 @@ +# Generated by Django 2.0.8 on 2018-09-20 18:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0011_auto_20180910_1902'), + ] + + operations = [ + migrations.CreateModel( + name='Delivery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_delivered', models.BooleanField(default=False)), + ('last_attempt_date', models.DateTimeField(blank=True, null=True)), + ('attempts', models.PositiveIntegerField(default=0)), + ('inbox_url', models.URLField(max_length=500)), + ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='deliveries', to='federation.Activity')), + ], + ), + migrations.RemoveField( + model_name='inboxitem', + name='delivery_attempts', + ), + migrations.RemoveField( + model_name='inboxitem', + name='is_delivered', + ), + migrations.RemoveField( + model_name='inboxitem', + name='last_delivery_date', + ), + ] diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 17ae01376..058bb9c46 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -3,14 +3,20 @@ import uuid from django.conf import settings from django.contrib.postgres.fields import JSONField +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.utils import timezone +from django.urls import reverse from funkwhale_api.common import session from funkwhale_api.common import utils as common_utils from funkwhale_api.music import utils as music_utils +from . import utils as federation_utils + TYPE_CHOICES = [ ("Person", "Person"), ("Application", "Application"), @@ -20,15 +26,47 @@ TYPE_CHOICES = [ ] +def empty_dict(): + return {} + + +def get_shared_inbox_url(): + return federation_utils.full_url(reverse("federation:shared-inbox")) + + +class FederationMixin(models.Model): + # federation id/url + fid = models.URLField(unique=True, max_length=500, db_index=True) + url = models.URLField(max_length=500, null=True, blank=True) + + class Meta: + abstract = True + + class ActorQuerySet(models.QuerySet): def local(self, include=True): return self.exclude(user__isnull=include) + def with_current_usage(self): + qs = self + for s in ["pending", "skipped", "errored", "finished"]: + qs = qs.annotate( + **{ + "_usage_{}".format(s): models.Sum( + "libraries__uploads__size", + filter=models.Q(libraries__uploads__import_status=s), + ) + } + ) + + return qs + class Actor(models.Model): ap_type = "Actor" - url = models.URLField(unique=True, max_length=500, db_index=True) + fid = models.URLField(unique=True, max_length=500, db_index=True) + url = models.URLField(max_length=500, null=True, blank=True) outbox_url = models.URLField(max_length=500) inbox_url = models.URLField(max_length=500) following_url = models.URLField(max_length=500, null=True, blank=True) @@ -39,8 +77,8 @@ class Actor(models.Model): 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) - public_key = models.CharField(max_length=5000, null=True, blank=True) - private_key = models.CharField(max_length=5000, null=True, blank=True) + public_key = models.TextField(max_length=5000, null=True, blank=True) + private_key = models.TextField(max_length=5000, null=True, blank=True) creation_date = models.DateTimeField(default=timezone.now) last_fetch_date = models.DateTimeField(default=timezone.now) manually_approves_followers = models.NullBooleanField(default=None) @@ -63,11 +101,14 @@ class Actor(models.Model): @property def private_key_id(self): - return "{}#main-key".format(self.url) + return "{}#main-key".format(self.fid) @property - def mention_username(self): - return "@{}@{}".format(self.preferred_username, self.domain) + def full_username(self): + return "{}@{}".format(self.preferred_username, self.domain) + + def __str__(self): + return "{}@{}".format(self.preferred_username, self.domain) def save(self, **kwargs): lowercase_fields = ["domain"] @@ -104,26 +145,137 @@ class Actor(models.Model): follows = self.received_follows.filter(approved=True) return self.followers.filter(pk__in=follows.values_list("actor", flat=True)) + def should_autoapprove_follow(self, actor): + return False -class Follow(models.Model): - ap_type = "Follow" + def get_user(self): + try: + return self.user + except ObjectDoesNotExist: + return None + def get_current_usage(self): + actor = self.__class__.objects.filter(pk=self.pk).with_current_usage().get() + data = {} + for s in ["pending", "skipped", "errored", "finished"]: + data[s] = getattr(actor, "_usage_{}".format(s)) or 0 + + data["total"] = sum(data.values()) + return data + + +class InboxItem(models.Model): + """ + Store activities binding to local actors, with read/unread status. + """ + + actor = models.ForeignKey( + Actor, related_name="inbox_items", on_delete=models.CASCADE + ) + activity = models.ForeignKey( + "Activity", related_name="inbox_items", on_delete=models.CASCADE + ) + type = models.CharField(max_length=10, choices=[("to", "to"), ("cc", "cc")]) + is_read = models.BooleanField(default=False) + + +class Delivery(models.Model): + """ + Store deliveries attempt to remote inboxes + """ + + is_delivered = models.BooleanField(default=False) + last_attempt_date = models.DateTimeField(null=True, blank=True) + attempts = models.PositiveIntegerField(default=0) + inbox_url = models.URLField(max_length=500) + + activity = models.ForeignKey( + "Activity", related_name="deliveries", on_delete=models.CASCADE + ) + + +class Activity(models.Model): + actor = models.ForeignKey( + Actor, related_name="outbox_activities", on_delete=models.CASCADE + ) + recipients = models.ManyToManyField( + Actor, related_name="inbox_activities", through=InboxItem + ) uuid = models.UUIDField(default=uuid.uuid4, unique=True) + fid = models.URLField(unique=True, max_length=500, null=True, blank=True) + url = models.URLField(max_length=500, null=True, blank=True) + payload = JSONField(default=empty_dict, max_length=50000, encoder=DjangoJSONEncoder) + creation_date = models.DateTimeField(default=timezone.now, db_index=True) + type = models.CharField(db_index=True, null=True, max_length=100) + + # generic relations + object_id = models.IntegerField(null=True) + object_content_type = models.ForeignKey( + ContentType, + null=True, + on_delete=models.SET_NULL, + related_name="objecting_activities", + ) + object = GenericForeignKey("object_content_type", "object_id") + target_id = models.IntegerField(null=True) + target_content_type = models.ForeignKey( + ContentType, + null=True, + on_delete=models.SET_NULL, + related_name="targeting_activities", + ) + target = GenericForeignKey("target_content_type", "target_id") + related_object_id = models.IntegerField(null=True) + related_object_content_type = models.ForeignKey( + ContentType, + null=True, + on_delete=models.SET_NULL, + related_name="related_objecting_activities", + ) + related_object = GenericForeignKey( + "related_object_content_type", "related_object_id" + ) + + +class AbstractFollow(models.Model): + ap_type = "Follow" + fid = models.URLField(unique=True, max_length=500, null=True, blank=True) + uuid = models.UUIDField(default=uuid.uuid4, unique=True) + creation_date = models.DateTimeField(default=timezone.now) + modification_date = models.DateTimeField(auto_now=True) + approved = models.NullBooleanField(default=None) + + class Meta: + abstract = True + + def get_federation_id(self): + return federation_utils.full_url( + "{}#follows/{}".format(self.actor.fid, self.uuid) + ) + + +class Follow(AbstractFollow): actor = models.ForeignKey( Actor, related_name="emitted_follows", on_delete=models.CASCADE ) target = models.ForeignKey( Actor, related_name="received_follows", on_delete=models.CASCADE ) - creation_date = models.DateTimeField(default=timezone.now) - modification_date = models.DateTimeField(auto_now=True) - approved = models.NullBooleanField(default=None) class Meta: unique_together = ["actor", "target"] - def get_federation_url(self): - return "{}#follows/{}".format(self.actor.url, self.uuid) + +class LibraryFollow(AbstractFollow): + actor = models.ForeignKey( + Actor, related_name="library_follows", on_delete=models.CASCADE + ) + target = models.ForeignKey( + "music.Library", related_name="received_follows", on_delete=models.CASCADE + ) + + class Meta: + unique_together = ["actor", "target"] class Library(models.Model): @@ -167,7 +319,9 @@ class LibraryTrack(models.Model): 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=empty_dict, max_length=10000, encoder=DjangoJSONEncoder + ) @property def mbid(self): diff --git a/api/funkwhale_api/federation/permissions.py b/api/funkwhale_api/federation/permissions.py deleted file mode 100644 index a08d57e5f..000000000 --- a/api/funkwhale_api/federation/permissions.py +++ /dev/null @@ -1,19 +0,0 @@ - -from rest_framework.permissions import BasePermission - -from funkwhale_api.common import preferences - -from . import actors - - -class LibraryFollower(BasePermission): - def has_permission(self, request, view): - if not preferences.get("federation__music_needs_approval"): - return True - - 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() diff --git a/api/funkwhale_api/federation/routes.py b/api/funkwhale_api/federation/routes.py new file mode 100644 index 000000000..b757d4f92 --- /dev/null +++ b/api/funkwhale_api/federation/routes.py @@ -0,0 +1,232 @@ +import logging + +from funkwhale_api.music import models as music_models + +from . import activity +from . import serializers + +logger = logging.getLogger(__name__) +inbox = activity.InboxRouter() +outbox = activity.OutboxRouter() + + +def with_recipients(payload, to=[], cc=[]): + if to: + payload["to"] = to + if cc: + payload["cc"] = cc + return payload + + +@inbox.register({"type": "Follow"}) +def inbox_follow(payload, context): + context["recipient"] = [ + ii.actor for ii in context["inbox_items"] if ii.type == "to" + ][0] + serializer = serializers.FollowSerializer(data=payload, context=context) + if not serializer.is_valid(raise_exception=context.get("raise_exception", False)): + logger.debug( + "Discarding invalid follow from {}: %s", + context["actor"].fid, + serializer.errors, + ) + return + + autoapprove = serializer.validated_data["object"].should_autoapprove_follow( + context["actor"] + ) + follow = serializer.save(approved=True if autoapprove else None) + if follow.approved: + outbox.dispatch({"type": "Accept"}, context={"follow": follow}) + return {"object": follow.target, "related_object": follow} + + +@inbox.register({"type": "Accept"}) +def inbox_accept(payload, context): + context["recipient"] = [ + ii.actor for ii in context["inbox_items"] if ii.type == "to" + ][0] + serializer = serializers.AcceptFollowSerializer(data=payload, context=context) + if not serializer.is_valid(raise_exception=context.get("raise_exception", False)): + logger.debug( + "Discarding invalid accept from {}: %s", + context["actor"].fid, + serializer.errors, + ) + return + + serializer.save() + obj = serializer.validated_data["follow"] + return {"object": obj, "related_object": obj.target} + + +@outbox.register({"type": "Accept"}) +def outbox_accept(context): + follow = context["follow"] + if follow._meta.label == "federation.LibraryFollow": + actor = follow.target.actor + else: + actor = follow.target + payload = serializers.AcceptFollowSerializer(follow, context={"actor": actor}).data + yield { + "actor": actor, + "type": "Accept", + "payload": with_recipients(payload, to=[follow.actor]), + "object": follow, + "related_object": follow.target, + } + + +@inbox.register({"type": "Undo", "object.type": "Follow"}) +def inbox_undo_follow(payload, context): + serializer = serializers.UndoFollowSerializer(data=payload, context=context) + if not serializer.is_valid(raise_exception=context.get("raise_exception", False)): + logger.debug( + "Discarding invalid follow undo from {}: %s", + context["actor"].fid, + serializer.errors, + ) + return + + serializer.save() + + +@outbox.register({"type": "Undo", "object.type": "Follow"}) +def outbox_undo_follow(context): + follow = context["follow"] + actor = follow.actor + if follow._meta.label == "federation.LibraryFollow": + recipient = follow.target.actor + else: + recipient = follow.target + payload = serializers.UndoFollowSerializer(follow, context={"actor": actor}).data + yield { + "actor": actor, + "type": "Undo", + "payload": with_recipients(payload, to=[recipient]), + "object": follow, + "related_object": follow.target, + } + + +@outbox.register({"type": "Follow"}) +def outbox_follow(context): + follow = context["follow"] + if follow._meta.label == "federation.LibraryFollow": + target = follow.target.actor + else: + target = follow.target + payload = serializers.FollowSerializer(follow, context={"actor": follow.actor}).data + yield { + "type": "Follow", + "actor": follow.actor, + "payload": with_recipients(payload, to=[target]), + "object": follow.target, + "related_object": follow, + } + + +@outbox.register({"type": "Create", "object.type": "Audio"}) +def outbox_create_audio(context): + upload = context["upload"] + serializer = serializers.ActivitySerializer( + { + "type": "Create", + "actor": upload.library.actor.fid, + "object": serializers.UploadSerializer(upload).data, + } + ) + yield { + "type": "Create", + "actor": upload.library.actor, + "payload": with_recipients( + serializer.data, to=[{"type": "followers", "target": upload.library}] + ), + "object": upload, + "target": upload.library, + } + + +@inbox.register({"type": "Create", "object.type": "Audio"}) +def inbox_create_audio(payload, context): + serializer = serializers.UploadSerializer( + data=payload["object"], + context={"activity": context.get("activity"), "actor": context["actor"]}, + ) + + if not serializer.is_valid(raise_exception=context.get("raise_exception", False)): + logger.warn("Discarding invalid audio create") + return + + upload = serializer.save() + + return {"object": upload, "target": upload.library} + + +@inbox.register({"type": "Delete", "object.type": "Library"}) +def inbox_delete_library(payload, context): + actor = context["actor"] + library_id = payload["object"].get("id") + if not library_id: + logger.debug("Discarding deletion of empty library") + return + + try: + library = actor.libraries.get(fid=library_id) + except music_models.Library.DoesNotExist: + logger.debug("Discarding deletion of unkwnown library %s", library_id) + return + + library.delete() + + +@outbox.register({"type": "Delete", "object.type": "Library"}) +def outbox_delete_library(context): + library = context["library"] + serializer = serializers.ActivitySerializer( + {"type": "Delete", "object": {"type": "Library", "id": library.fid}} + ) + yield { + "type": "Delete", + "actor": library.actor, + "payload": with_recipients( + serializer.data, to=[{"type": "followers", "target": library}] + ), + } + + +@inbox.register({"type": "Delete", "object.type": "Audio"}) +def inbox_delete_audio(payload, context): + actor = context["actor"] + try: + upload_fids = [i for i in payload["object"]["id"]] + except TypeError: + # we did not receive a list of Ids, so we can probably use the value directly + upload_fids = [payload["object"]["id"]] + + candidates = music_models.Upload.objects.filter( + library__actor=actor, fid__in=upload_fids + ) + + total = candidates.count() + logger.info("Deleting %s uploads with ids %s", total, upload_fids) + candidates.delete() + + +@outbox.register({"type": "Delete", "object.type": "Audio"}) +def outbox_delete_audio(context): + uploads = context["uploads"] + library = uploads[0].library + serializer = serializers.ActivitySerializer( + { + "type": "Delete", + "object": {"type": "Audio", "id": [u.get_federation_id() for u in uploads]}, + } + ) + yield { + "type": "Delete", + "actor": library.actor, + "payload": with_recipients( + serializer.data, to=[{"type": "followers", "target": library}] + ), + } diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 304253aa0..61574a57e 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -4,15 +4,12 @@ import urllib.parse from django.core.exceptions import ObjectDoesNotExist from django.core.paginator import Paginator -from django.db import transaction from rest_framework import serializers -from funkwhale_api.common import serializers as common_serializers from funkwhale_api.common import utils as funkwhale_utils from funkwhale_api.music import models as music_models -from funkwhale_api.music import tasks as music_tasks -from . import activity, filters, models, utils +from . import activity, models, utils AP_CONTEXT = [ "https://www.w3.org/ns/activitystreams", @@ -23,6 +20,31 @@ AP_CONTEXT = [ logger = logging.getLogger(__name__) +class LinkSerializer(serializers.Serializer): + type = serializers.ChoiceField(choices=["Link"]) + href = serializers.URLField(max_length=500) + mediaType = serializers.CharField() + + def __init__(self, *args, **kwargs): + self.allowed_mimetypes = kwargs.pop("allowed_mimetypes", []) + super().__init__(*args, **kwargs) + + def validate_mediaType(self, v): + if not self.allowed_mimetypes: + # no restrictions + return v + for mt in self.allowed_mimetypes: + if mt.endswith("/*"): + if v.startswith(mt.replace("*", "")): + return v + else: + if v == mt: + return v + raise serializers.ValidationError( + "Invalid mimetype {}. Allowed: {}".format(v, self.allowed_mimetypes) + ) + + class ActorSerializer(serializers.Serializer): id = serializers.URLField(max_length=500) outbox = serializers.URLField(max_length=500) @@ -32,13 +54,13 @@ class ActorSerializer(serializers.Serializer): manuallyApprovesFollowers = serializers.NullBooleanField(required=False) name = serializers.CharField(required=False, max_length=200) summary = serializers.CharField(max_length=None, required=False) - followers = serializers.URLField(max_length=500, required=False, allow_null=True) + followers = serializers.URLField(max_length=500) following = serializers.URLField(max_length=500, required=False, allow_null=True) publicKey = serializers.JSONField(required=False) def to_representation(self, instance): ret = { - "id": instance.url, + "id": instance.fid, "outbox": instance.outbox_url, "inbox": instance.inbox_url, "preferredUsername": instance.preferred_username, @@ -58,9 +80,9 @@ class ActorSerializer(serializers.Serializer): ret["@context"] = AP_CONTEXT if instance.public_key: ret["publicKey"] = { - "owner": instance.url, + "owner": instance.fid, "publicKeyPem": instance.public_key, - "id": "{}#main-key".format(instance.url), + "id": "{}#main-key".format(instance.fid), } ret["endpoints"] = {} if instance.shared_inbox_url: @@ -78,7 +100,7 @@ class ActorSerializer(serializers.Serializer): def prepare_missing_fields(self): kwargs = { - "url": self.validated_data["id"], + "fid": self.validated_data["id"], "outbox_url": self.validated_data["outbox"], "inbox_url": self.validated_data["inbox"], "following_url": self.validated_data.get("following"), @@ -91,7 +113,7 @@ class ActorSerializer(serializers.Serializer): maf = self.validated_data.get("manuallyApprovesFollowers") if maf is not None: kwargs["manually_approves_followers"] = maf - domain = urllib.parse.urlparse(kwargs["url"]).netloc + domain = urllib.parse.urlparse(kwargs["fid"]).netloc kwargs["domain"] = domain for endpoint, url in self.initial_data.get("endpoints", {}).items(): if endpoint == "sharedInbox": @@ -110,7 +132,7 @@ class ActorSerializer(serializers.Serializer): def save(self, **kwargs): d = self.prepare_missing_fields() d.update(kwargs) - return models.Actor.objects.update_or_create(url=d["url"], defaults=d)[0] + return models.Actor.objects.update_or_create(fid=d["fid"], defaults=d)[0] def validate_summary(self, value): if value: @@ -122,6 +144,7 @@ class APIActorSerializer(serializers.ModelSerializer): model = models.Actor fields = [ "id", + "fid", "url", "creation_date", "summary", @@ -131,190 +154,50 @@ class APIActorSerializer(serializers.ModelSerializer): "domain", "type", "manually_approves_followers", + "full_username", ] -class LibraryActorSerializer(ActorSerializer): - url = serializers.ListField(child=serializers.JSONField()) - - def validate(self, validated_data): - try: - urls = validated_data["url"] - except KeyError: - raise serializers.ValidationError("Missing URL field") - - for u in urls: - try: - if u["name"] != "library": - continue - validated_data["library_url"] = u["href"] - break - except KeyError: - continue - - return validated_data - - -class APIFollowSerializer(serializers.ModelSerializer): - class Meta: - model = models.Follow - fields = [ - "uuid", - "actor", - "target", - "approved", - "creation_date", - "modification_date", - ] - - -class APILibrarySerializer(serializers.ModelSerializer): - actor = APIActorSerializer() - follow = APIFollowSerializer() - - class Meta: - model = models.Library - - read_only_fields = [ - "actor", - "uuid", - "url", - "tracks_count", - "follow", - "fetched_date", - "modification_date", - "creation_date", - ] - fields = [ - "autoimport", - "federation_enabled", - "download_files", - ] + read_only_fields - - -class APILibraryScanSerializer(serializers.Serializer): - until = serializers.DateTimeField(required=False) - - -class APILibraryFollowUpdateSerializer(serializers.Serializer): - follow = serializers.IntegerField() - approved = serializers.BooleanField() - - def validate_follow(self, value): - from . import actors - - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - qs = models.Follow.objects.filter(pk=value, target=library_actor) - try: - return qs.get() - except models.Follow.DoesNotExist: - raise serializers.ValidationError("Invalid follow") - - def save(self): - new_status = self.validated_data["approved"] - follow = self.validated_data["follow"] - if new_status == follow.approved: - return follow - - follow.approved = new_status - follow.save(update_fields=["approved", "modification_date"]) - if new_status: - activity.accept_follow(follow) - return follow - - -class APILibraryCreateSerializer(serializers.ModelSerializer): +class BaseActivitySerializer(serializers.Serializer): + id = serializers.URLField(max_length=500, required=False) + type = serializers.CharField(max_length=100) actor = serializers.URLField(max_length=500) - federation_enabled = serializers.BooleanField() - uuid = serializers.UUIDField(read_only=True) - class Meta: - model = models.Library - fields = ["uuid", "actor", "autoimport", "federation_enabled", "download_files"] - - def validate(self, validated_data): - from . import actors - from . import library - - actor_url = validated_data["actor"] - actor_data = actors.get_actor_data(actor_url) - acs = LibraryActorSerializer(data=actor_data) - acs.is_valid(raise_exception=True) + def validate_actor(self, v): + expected = self.context.get("actor") + if expected and expected.fid != v: + raise serializers.ValidationError("Invalid actor") + if expected: + # avoid a DB lookup + return expected try: - actor = models.Actor.objects.get(url=actor_url) + return models.Actor.objects.get(fid=v) except models.Actor.DoesNotExist: - actor = acs.save() - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - validated_data["follow"] = models.Follow.objects.get_or_create( - actor=library_actor, target=actor - )[0] - if validated_data["follow"].approved is None: - funkwhale_utils.on_commit( - activity.deliver, - FollowSerializer(validated_data["follow"]).data, - on_behalf_of=validated_data["follow"].actor, - to=[validated_data["follow"].target.url], - ) - - library_data = library.get_library_data(acs.validated_data["library_url"]) - if "errors" in library_data: - # we pass silently because it may means we require permission - # before scanning - pass - validated_data["library"] = library_data - validated_data["library"].setdefault("id", acs.validated_data["library_url"]) - validated_data["actor"] = actor - return validated_data + raise serializers.ValidationError("Actor not found") def create(self, validated_data): - library = models.Library.objects.update_or_create( - url=validated_data["library"]["id"], - defaults={ - "actor": validated_data["actor"], - "follow": validated_data["follow"], - "tracks_count": validated_data["library"].get("totalItems"), - "federation_enabled": validated_data["federation_enabled"], - "autoimport": validated_data["autoimport"], - "download_files": validated_data["download_files"], - }, - )[0] - return library + return models.Activity.objects.create( + fid=validated_data.get("id"), + actor=validated_data["actor"], + payload=self.initial_data, + type=validated_data["type"], + ) + def validate(self, data): + data["recipients"] = self.validate_recipients(self.initial_data) + return super().validate(data) -class APILibraryTrackSerializer(serializers.ModelSerializer): - library = APILibrarySerializer() - status = serializers.SerializerMethodField() + def validate_recipients(self, payload): + """ + Ensure we have at least a to/cc field with valid actors + """ + to = payload.get("to", []) + cc = payload.get("cc", []) - class Meta: - model = models.LibraryTrack - fields = [ - "id", - "url", - "audio_url", - "audio_mimetype", - "creation_date", - "modification_date", - "fetched_date", - "published_date", - "metadata", - "artist_name", - "album_title", - "title", - "library", - "local_track_file", - "status", - ] - - def get_status(self, o): - try: - if o.local_track_file is not None: - return "imported" - except music_models.TrackFile.DoesNotExist: - pass - for job in o.import_jobs.all(): - if job.status == "pending": - return "import_pending" - return "not_imported" + if not to and not cc: + raise serializers.ValidationError( + "We cannot handle an activity with no recipient" + ) class FollowSerializer(serializers.Serializer): @@ -325,35 +208,61 @@ class FollowSerializer(serializers.Serializer): def validate_object(self, v): expected = self.context.get("follow_target") - if expected and expected.url != v: + if self.parent: + # it's probably an accept, so everything is inverted, the actor + # the recipient does not matter + recipient = None + else: + recipient = self.context.get("recipient") + if expected and expected.fid != v: raise serializers.ValidationError("Invalid target") try: - return models.Actor.objects.get(url=v) + obj = models.Actor.objects.get(fid=v) + if recipient and recipient.fid != obj.fid: + raise serializers.ValidationError("Invalid target") + return obj except models.Actor.DoesNotExist: - raise serializers.ValidationError("Target not found") + pass + try: + qs = music_models.Library.objects.filter(fid=v) + if recipient: + qs = qs.filter(actor=recipient) + return qs.get() + except music_models.Library.DoesNotExist: + pass + + raise serializers.ValidationError("Target not found") def validate_actor(self, v): expected = self.context.get("follow_actor") - if expected and expected.url != v: + if expected and expected.fid != v: raise serializers.ValidationError("Invalid actor") try: - return models.Actor.objects.get(url=v) + return models.Actor.objects.get(fid=v) except models.Actor.DoesNotExist: raise serializers.ValidationError("Actor not found") def save(self, **kwargs): - return models.Follow.objects.get_or_create( + target = self.validated_data["object"] + + if target._meta.label == "music.Library": + follow_class = models.LibraryFollow + else: + follow_class = models.Follow + defaults = kwargs + defaults["fid"] = self.validated_data["id"] + return follow_class.objects.update_or_create( actor=self.validated_data["actor"], target=self.validated_data["object"], - **kwargs, # noqa + defaults=defaults, )[0] def to_representation(self, instance): return { "@context": AP_CONTEXT, - "actor": instance.actor.url, - "id": instance.get_federation_url(), - "object": instance.target.url, + "actor": instance.actor.fid, + "id": instance.get_federation_id(), + "object": instance.target.fid, "type": "Follow", } @@ -376,50 +285,66 @@ class APIFollowSerializer(serializers.ModelSerializer): class AcceptFollowSerializer(serializers.Serializer): - id = serializers.URLField(max_length=500) + id = serializers.URLField(max_length=500, required=False) actor = serializers.URLField(max_length=500) object = FollowSerializer() type = serializers.ChoiceField(choices=["Accept"]) def validate_actor(self, v): - expected = self.context.get("follow_target") - if expected and expected.url != v: + expected = self.context.get("actor") + if expected and expected.fid != v: raise serializers.ValidationError("Invalid actor") try: - return models.Actor.objects.get(url=v) + return models.Actor.objects.get(fid=v) except models.Actor.DoesNotExist: raise serializers.ValidationError("Actor not found") def validate(self, validated_data): - # we ensure the accept actor actually match the follow target - if validated_data["actor"] != validated_data["object"]["object"]: + # we ensure the accept actor actually match the follow target / library owner + target = validated_data["object"]["object"] + + if target._meta.label == "music.Library": + expected = target.actor + follow_class = models.LibraryFollow + else: + expected = target + follow_class = models.Follow + if validated_data["actor"] != expected: raise serializers.ValidationError("Actor mismatch") try: validated_data["follow"] = ( - models.Follow.objects.filter( - target=validated_data["actor"], - actor=validated_data["object"]["actor"], + follow_class.objects.filter( + target=target, actor=validated_data["object"]["actor"] ) .exclude(approved=True) + .select_related() .get() ) - except models.Follow.DoesNotExist: + except follow_class.DoesNotExist: raise serializers.ValidationError("No follow to accept") return validated_data def to_representation(self, instance): + if instance.target._meta.label == "music.Library": + actor = instance.target.actor + else: + actor = instance.target + return { "@context": AP_CONTEXT, - "id": instance.get_federation_url() + "/accept", + "id": instance.get_federation_id() + "/accept", "type": "Accept", - "actor": instance.target.url, + "actor": actor.fid, "object": FollowSerializer(instance).data, } def save(self): - self.validated_data["follow"].approved = True - self.validated_data["follow"].save() - return self.validated_data["follow"] + follow = self.validated_data["follow"] + follow.approved = True + follow.save() + if follow.target._meta.label == "music.Library": + follow.target.schedule_scan(actor=follow.actor) + return follow class UndoFollowSerializer(serializers.Serializer): @@ -429,11 +354,12 @@ class UndoFollowSerializer(serializers.Serializer): type = serializers.ChoiceField(choices=["Undo"]) def validate_actor(self, v): - expected = self.context.get("follow_target") - if expected and expected.url != v: + expected = self.context.get("actor") + + if expected and expected.fid != v: raise serializers.ValidationError("Invalid actor") try: - return models.Actor.objects.get(url=v) + return models.Actor.objects.get(fid=v) except models.Actor.DoesNotExist: raise serializers.ValidationError("Actor not found") @@ -441,20 +367,28 @@ class UndoFollowSerializer(serializers.Serializer): # we ensure the accept actor actually match the follow actor if validated_data["actor"] != validated_data["object"]["actor"]: raise serializers.ValidationError("Actor mismatch") + + target = validated_data["object"]["object"] + + if target._meta.label == "music.Library": + follow_class = models.LibraryFollow + else: + follow_class = models.Follow + try: - validated_data["follow"] = models.Follow.objects.filter( - actor=validated_data["actor"], target=validated_data["object"]["object"] + validated_data["follow"] = follow_class.objects.filter( + actor=validated_data["actor"], target=target ).get() - except models.Follow.DoesNotExist: + except follow_class.DoesNotExist: raise serializers.ValidationError("No follow to remove") return validated_data def to_representation(self, instance): return { "@context": AP_CONTEXT, - "id": instance.get_federation_url() + "/undo", + "id": instance.get_federation_id() + "/undo", "type": "Undo", - "actor": instance.actor.url, + "actor": instance.actor.fid, "object": FollowSerializer(instance).data, } @@ -488,9 +422,9 @@ class ActorWebfingerSerializer(serializers.Serializer): data = {} data["subject"] = "acct:{}".format(instance.webfinger_subject) data["links"] = [ - {"rel": "self", "href": instance.url, "type": "application/activity+json"} + {"rel": "self", "href": instance.fid, "type": "application/activity+json"} ] - data["aliases"] = [instance.url] + data["aliases"] = [instance.fid] return data @@ -498,7 +432,8 @@ class ActivitySerializer(serializers.Serializer): actor = serializers.URLField(max_length=500) id = serializers.URLField(max_length=500, required=False) type = serializers.ChoiceField(choices=[(c, c) for c in activity.ACTIVITY_TYPES]) - object = serializers.JSONField() + object = serializers.JSONField(required=False) + target = serializers.JSONField(required=False) def validate_object(self, value): try: @@ -519,7 +454,7 @@ class ActivitySerializer(serializers.Serializer): def validate_actor(self, value): request_actor = self.context.get("actor") - if request_actor and request_actor.url != value: + if request_actor and request_actor.fid != value: raise serializers.ValidationError( "The actor making the request do not match" " the activity actor" ) @@ -560,6 +495,18 @@ class ObjectSerializer(serializers.Serializer): OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES} +def get_additional_fields(data): + UNSET = object() + additional_fields = {} + for field in ["name", "summary"]: + v = data.get(field, UNSET) + if v == UNSET: + continue + additional_fields[field] = v + + return additional_fields + + class PaginatedCollectionSerializer(serializers.Serializer): type = serializers.ChoiceField(choices=["Collection"]) totalItems = serializers.IntegerField(min_value=0) @@ -575,18 +522,73 @@ class PaginatedCollectionSerializer(serializers.Serializer): last = funkwhale_utils.set_query_parameter(conf["id"], page=paginator.num_pages) d = { "id": conf["id"], - "actor": conf["actor"].url, + "actor": conf["actor"].fid, "totalItems": paginator.count, - "type": "Collection", + "type": conf.get("type", "Collection"), "current": current, "first": first, "last": last, } + d.update(get_additional_fields(conf)) if self.context.get("include_ap_context", True): d["@context"] = AP_CONTEXT return d +class LibrarySerializer(PaginatedCollectionSerializer): + type = serializers.ChoiceField(choices=["Library"]) + name = serializers.CharField() + summary = serializers.CharField(allow_blank=True, allow_null=True, required=False) + followers = serializers.URLField(max_length=500) + audience = serializers.ChoiceField( + choices=["", None, "https://www.w3.org/ns/activitystreams#Public"], + required=False, + allow_null=True, + allow_blank=True, + ) + + def to_representation(self, library): + conf = { + "id": library.fid, + "name": library.name, + "summary": library.description, + "page_size": 100, + "actor": library.actor, + "items": library.uploads.for_federation(), + "type": "Library", + } + r = super().to_representation(conf) + r["audience"] = ( + "https://www.w3.org/ns/activitystreams#Public" + if library.privacy_level == "public" + else "" + ) + r["followers"] = library.followers_url + return r + + def create(self, validated_data): + actor = utils.retrieve( + validated_data["actor"], + queryset=models.Actor, + serializer_class=ActorSerializer, + ) + library, created = music_models.Library.objects.update_or_create( + fid=validated_data["id"], + actor=actor, + defaults={ + "uploads_count": validated_data["totalItems"], + "name": validated_data["name"], + "description": validated_data["summary"], + "followers_url": validated_data["followers"], + "privacy_level": "everyone" + if validated_data["audience"] + == "https://www.w3.org/ns/activitystreams#Public" + else "me", + }, + ) + return library + + class CollectionPageSerializer(serializers.Serializer): type = serializers.ChoiceField(choices=["CollectionPage"]) totalItems = serializers.IntegerField(min_value=0) @@ -606,9 +608,10 @@ class CollectionPageSerializer(serializers.Serializer): raw_items = [item_serializer(data=i, context=self.context) for i in v] valid_items = [] for i in raw_items: - if i.is_valid(): + try: + i.is_valid(raise_exception=True) valid_items.append(i) - else: + except serializers.ValidationError: logger.debug("Invalid item %s: %s", i.data, i.errors) return valid_items @@ -623,7 +626,7 @@ class CollectionPageSerializer(serializers.Serializer): d = { "id": id, "partOf": conf["id"], - "actor": conf["actor"].url, + "actor": conf["actor"].fid, "totalItems": page.paginator.count, "type": "CollectionPage", "first": first, @@ -645,48 +648,135 @@ class CollectionPageSerializer(serializers.Serializer): d["next"] = funkwhale_utils.set_query_parameter( conf["id"], page=page.next_page_number() ) - + d.update(get_additional_fields(conf)) if self.context.get("include_ap_context", True): d["@context"] = AP_CONTEXT return d -class ArtistMetadataSerializer(serializers.Serializer): - musicbrainz_id = serializers.UUIDField(required=False, allow_null=True) - name = serializers.CharField() - - -class ReleaseMetadataSerializer(serializers.Serializer): - musicbrainz_id = serializers.UUIDField(required=False, allow_null=True) - title = serializers.CharField() - - -class RecordingMetadataSerializer(serializers.Serializer): - musicbrainz_id = serializers.UUIDField(required=False, allow_null=True) - title = serializers.CharField() - - -class AudioMetadataSerializer(serializers.Serializer): - artist = ArtistMetadataSerializer() - release = ReleaseMetadataSerializer() - recording = RecordingMetadataSerializer() - bitrate = serializers.IntegerField(required=False, allow_null=True, min_value=0) - size = serializers.IntegerField(required=False, allow_null=True, min_value=0) - length = serializers.IntegerField(required=False, allow_null=True, min_value=0) - - -class AudioSerializer(serializers.Serializer): - type = serializers.CharField() +class MusicEntitySerializer(serializers.Serializer): id = serializers.URLField(max_length=500) - url = serializers.JSONField() published = serializers.DateTimeField() - updated = serializers.DateTimeField(required=False) - metadata = AudioMetadataSerializer() + musicbrainzId = serializers.UUIDField(allow_null=True, required=False) + name = serializers.CharField(max_length=1000) - def validate_type(self, v): - if v != "Audio": - raise serializers.ValidationError("Invalid type for audio") - return v + +class ArtistSerializer(MusicEntitySerializer): + def to_representation(self, instance): + d = { + "type": "Artist", + "id": instance.fid, + "name": instance.name, + "published": instance.creation_date.isoformat(), + "musicbrainzId": str(instance.mbid) if instance.mbid else None, + } + + if self.context.get("include_ap_context", self.parent is None): + d["@context"] = AP_CONTEXT + return d + + +class AlbumSerializer(MusicEntitySerializer): + released = serializers.DateField(allow_null=True, required=False) + artists = serializers.ListField(child=ArtistSerializer(), min_length=1) + cover = LinkSerializer( + allowed_mimetypes=["image/*"], allow_null=True, required=False + ) + + def to_representation(self, instance): + d = { + "type": "Album", + "id": instance.fid, + "name": instance.title, + "published": instance.creation_date.isoformat(), + "musicbrainzId": str(instance.mbid) if instance.mbid else None, + "released": instance.release_date.isoformat() + if instance.release_date + else None, + "artists": [ + ArtistSerializer( + instance.artist, context={"include_ap_context": False} + ).data + ], + } + if instance.cover: + d["cover"] = { + "type": "Link", + "href": utils.full_url(instance.cover.url), + "mediaType": mimetypes.guess_type(instance.cover.path)[0] + or "image/jpeg", + } + if self.context.get("include_ap_context", self.parent is None): + d["@context"] = AP_CONTEXT + return d + + def get_create_data(self, validated_data): + artist_data = validated_data["artists"][0] + artist = ArtistSerializer( + context={"activity": self.context.get("activity")} + ).create(artist_data) + + return { + "mbid": validated_data.get("musicbrainzId"), + "fid": validated_data["id"], + "title": validated_data["name"], + "creation_date": validated_data["published"], + "artist": artist, + "release_date": validated_data.get("released"), + "from_activity": self.context.get("activity"), + } + + +class TrackSerializer(MusicEntitySerializer): + position = serializers.IntegerField(min_value=0, allow_null=True, required=False) + artists = serializers.ListField(child=ArtistSerializer(), min_length=1) + album = AlbumSerializer() + + def to_representation(self, instance): + d = { + "type": "Track", + "id": instance.fid, + "name": instance.title, + "published": instance.creation_date.isoformat(), + "musicbrainzId": str(instance.mbid) if instance.mbid else None, + "position": instance.position, + "artists": [ + ArtistSerializer( + instance.artist, context={"include_ap_context": False} + ).data + ], + "album": AlbumSerializer( + instance.album, context={"include_ap_context": False} + ).data, + } + + if self.context.get("include_ap_context", self.parent is None): + d["@context"] = AP_CONTEXT + return d + + def create(self, validated_data): + from funkwhale_api.music import tasks as music_tasks + + metadata = music_tasks.federation_audio_track_to_metadata(validated_data) + from_activity = self.context.get("activity") + if from_activity: + metadata["from_activity_id"] = from_activity.pk + track = music_tasks.get_track_from_import_metadata(metadata) + return track + + +class UploadSerializer(serializers.Serializer): + type = serializers.ChoiceField(choices=["Audio"]) + id = serializers.URLField(max_length=500) + library = serializers.URLField(max_length=500) + url = LinkSerializer(allowed_mimetypes=["audio/*"]) + published = serializers.DateTimeField() + updated = serializers.DateTimeField(required=False, allow_null=True) + bitrate = serializers.IntegerField(min_value=0) + size = serializers.IntegerField(min_value=0) + duration = serializers.IntegerField(min_value=0) + + track = TrackSerializer(required=True) def validate_url(self, v): try: @@ -704,57 +794,70 @@ class AudioSerializer(serializers.Serializer): return v + def validate_library(self, v): + lb = self.context.get("library") + if lb: + if lb.fid != v: + raise serializers.ValidationError("Invalid library") + return lb + + actor = self.context.get("actor") + kwargs = {} + if actor: + kwargs["actor"] = actor + try: + return music_models.Library.objects.get(fid=v, **kwargs) + except music_models.Library.DoesNotExist: + raise serializers.ValidationError("Invalid library") + def create(self, validated_data): - defaults = { - "audio_mimetype": validated_data["url"]["mediaType"], - "audio_url": validated_data["url"]["href"], - "metadata": validated_data["metadata"], - "artist_name": validated_data["metadata"]["artist"]["name"], - "album_title": validated_data["metadata"]["release"]["title"], - "title": validated_data["metadata"]["recording"]["title"], - "published_date": validated_data["published"], + try: + return music_models.Upload.objects.get(fid=validated_data["id"]) + except music_models.Upload.DoesNotExist: + pass + + track = TrackSerializer( + context={"activity": self.context.get("activity")} + ).create(validated_data["track"]) + + data = { + "fid": validated_data["id"], + "mimetype": validated_data["url"]["mediaType"], + "source": validated_data["url"]["href"], + "creation_date": validated_data["published"], "modification_date": validated_data.get("updated"), + "track": track, + "duration": validated_data["duration"], + "size": validated_data["size"], + "bitrate": validated_data["bitrate"], + "library": validated_data["library"], + "from_activity": self.context.get("activity"), + "import_status": "finished", } - return models.LibraryTrack.objects.get_or_create( - library=self.context["library"], url=validated_data["id"], defaults=defaults - )[0] + return music_models.Upload.objects.create(**data) def to_representation(self, instance): track = instance.track - album = instance.track.album - artist = instance.track.artist - d = { "type": "Audio", - "id": instance.get_federation_url(), - "name": instance.track.full_name, + "id": instance.get_federation_id(), + "library": instance.library.fid, + "name": track.full_name, "published": instance.creation_date.isoformat(), - "updated": instance.modification_date.isoformat(), - "metadata": { - "artist": { - "musicbrainz_id": str(artist.mbid) if artist.mbid else None, - "name": artist.name, - }, - "release": { - "musicbrainz_id": str(album.mbid) if album.mbid else None, - "title": album.title, - }, - "recording": { - "musicbrainz_id": str(track.mbid) if track.mbid else None, - "title": track.title, - }, - "bitrate": instance.bitrate, - "size": instance.size, - "length": instance.duration, - }, + "bitrate": instance.bitrate, + "size": instance.size, + "duration": instance.duration, "url": { - "href": utils.full_url(instance.path), + "href": utils.full_url(instance.listen_url), "type": "Link", "mediaType": instance.mimetype, }, - "attributedTo": [self.context["actor"].url], + "track": TrackSerializer(track, context={"include_ap_context": False}).data, } - if self.context.get("include_ap_context", True): + if instance.modification_date: + d["updated"] = instance.modification_date.isoformat() + + if self.context.get("include_ap_context", self.parent is None): d["@context"] = AP_CONTEXT return d @@ -763,7 +866,7 @@ class CollectionSerializer(serializers.Serializer): def to_representation(self, conf): d = { "id": conf["id"], - "actor": conf["actor"].url, + "actor": conf["actor"].fid, "totalItems": len(conf["items"]), "type": "Collection", "items": [ @@ -777,27 +880,3 @@ class CollectionSerializer(serializers.Serializer): if self.context.get("include_ap_context", True): d["@context"] = AP_CONTEXT return d - - -class LibraryTrackActionSerializer(common_serializers.ActionSerializer): - actions = [common_serializers.Action("import", allow_all=True)] - filterset_class = filters.LibraryTrackFilter - - @transaction.atomic - def handle_import(self, objects): - batch = music_models.ImportBatch.objects.create( - source="federation", submitted_by=self.context["submitted_by"] - ) - jobs = [] - for lt in objects: - job = music_models.ImportJob( - batch=batch, library_track=lt, mbid=lt.mbid, source=lt.url - ) - jobs.append(job) - - music_models.ImportJob.objects.bulk_create(jobs) - funkwhale_utils.on_commit( - music_tasks.import_batch_run.delay, import_batch_id=batch.pk - ) - - return {"batch": {"id": batch.pk}} diff --git a/api/funkwhale_api/federation/tasks.py b/api/funkwhale_api/federation/tasks.py index d1b5b7bd2..33f94cad3 100644 --- a/api/funkwhale_api/federation/tasks.py +++ b/api/funkwhale_api/federation/tasks.py @@ -1,92 +1,24 @@ import datetime -import json import logging import os from django.conf import settings -from django.db.models import Q +from django.db.models import Q, F from django.utils import timezone from dynamic_preferences.registries import global_preferences_registry from requests.exceptions import RequestException +from funkwhale_api.common import preferences from funkwhale_api.common import session +from funkwhale_api.music import models as music_models from funkwhale_api.taskapp import celery -from . import actors -from . import library as lb from . import models, signing +from . import routes logger = logging.getLogger(__name__) -@celery.app.task( - name="federation.send", - autoretry_for=[RequestException], - retry_backoff=30, - 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) - 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)) - 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"}, - ) - response.raise_for_status() - logger.debug("Remote answered with %s", response.status_code) - - -@celery.app.task( - name="federation.scan_library", - autoretry_for=[RequestException], - retry_backoff=30, - 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) - library.fetched_date = timezone.now() - library.tracks_count = data["totalItems"] - library.save(update_fields=["fetched_date", "tracks_count"]) - - -@celery.app.task( - name="federation.scan_library_page", - autoretry_for=[RequestException], - retry_backoff=30, - 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"] - if until and item_date < until: - return - lts.append(item_serializer.save()) - - 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") def clean_music_cache(): preferences = global_preferences_registry.manager() @@ -96,23 +28,23 @@ def clean_music_cache(): limit = timezone.now() - datetime.timedelta(minutes=delay) candidates = ( - models.LibraryTrack.objects.filter( + music_models.Upload.objects.filter( Q(audio_file__isnull=False) - & ( - Q(local_track_file__accessed_date__lt=limit) - | Q(local_track_file__accessed_date=None) - ) + & (Q(accessed_date__lt=limit) | Q(accessed_date=None)), + # library__actor__user=None, ) + .local(False) .exclude(audio_file="") .only("audio_file", "id") + .order_by("id") ) - for lt in candidates: - lt.audio_file.delete() + for upload in candidates: + upload.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") - existing = models.LibraryTrack.objects.filter(audio_file__in=files) + files = get_files(storage, "federation_cache/tracks") + existing = music_models.Upload.objects.filter(audio_file__in=files) missing = set(files) - set(existing.values_list("audio_file", flat=True)) for m in missing: storage.delete(m) @@ -125,8 +57,93 @@ def get_files(storage, *parts): """ if not parts: raise ValueError("Missing path") - - dirs, files = storage.listdir(os.path.join(*parts)) + try: + dirs, files = storage.listdir(os.path.join(*parts)) + except FileNotFoundError: + return [] for dir in dirs: files += get_files(storage, *(list(parts) + [dir])) return [os.path.join(parts[-1], path) for path in files] + + +@celery.app.task(name="federation.dispatch_inbox") +@celery.require_instance(models.Activity.objects.select_related(), "activity") +def dispatch_inbox(activity): + """ + Given an activity instance, triggers our internal delivery logic (follow + creation, etc.) + """ + + routes.inbox.dispatch( + activity.payload, + context={ + "activity": activity, + "actor": activity.actor, + "inbox_items": activity.inbox_items.filter(is_read=False), + }, + ) + + +@celery.app.task(name="federation.dispatch_outbox") +@celery.require_instance(models.Activity.objects.select_related(), "activity") +def dispatch_outbox(activity): + """ + Deliver a local activity to its recipients, both locally and remotely + """ + inbox_items = activity.inbox_items.filter(is_read=False).select_related() + + if inbox_items.exists(): + dispatch_inbox.delay(activity_id=activity.pk) + + if not preferences.get("federation__enabled"): + # federation is disabled, we only deliver to local recipients + return + + deliveries = activity.deliveries.filter(is_delivered=False) + + for id in deliveries.values_list("pk", flat=True): + deliver_to_remote.delay(delivery_id=id) + + +@celery.app.task( + name="federation.deliver_to_remote_inbox", + autoretry_for=[RequestException], + retry_backoff=30, + max_retries=5, +) +@celery.require_instance( + models.Delivery.objects.filter(is_delivered=False).select_related( + "activity__actor" + ), + "delivery", +) +def deliver_to_remote(delivery): + + if not preferences.get("federation__enabled"): + # federation is disabled, we only deliver to local recipients + return + + actor = delivery.activity.actor + logger.info("Preparing activity delivery to %s", delivery.inbox_url) + auth = signing.get_auth(actor.private_key, actor.private_key_id) + try: + response = session.get_session().post( + auth=auth, + json=delivery.activity.payload, + url=delivery.inbox_url, + timeout=5, + verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, + headers={"Content-Type": "application/activity+json"}, + ) + logger.debug("Remote answered with %s", response.status_code) + response.raise_for_status() + except Exception: + delivery.last_attempt_date = timezone.now() + delivery.attempts = F("attempts") + 1 + delivery.save(update_fields=["last_attempt_date", "attempts"]) + raise + else: + delivery.last_attempt_date = timezone.now() + delivery.attempts = F("attempts") + 1 + delivery.is_delivered = True + delivery.save(update_fields=["last_attempt_date", "attempts", "is_delivered"]) diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py index 319e37bed..f8347d1eb 100644 --- a/api/funkwhale_api/federation/urls.py +++ b/api/funkwhale_api/federation/urls.py @@ -5,13 +5,16 @@ 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"federation/shared", views.SharedViewSet, "shared") router.register(r"federation/actors", views.ActorViewSet, "actors") router.register(r".well-known", views.WellKnownViewSet, "well-known") -music_router.register(r"files", views.MusicFilesViewSet, "files") +music_router.register(r"libraries", views.MusicLibraryViewSet, "libraries") +music_router.register(r"uploads", views.MusicUploadViewSet, "uploads") +music_router.register(r"artists", views.MusicArtistViewSet, "artists") +music_router.register(r"albums", views.MusicAlbumViewSet, "albums") +music_router.register(r"tracks", views.MusicTrackViewSet, "tracks") urlpatterns = router.urls + [ url("federation/music/", include((music_router.urls, "music"), namespace="music")) ] diff --git a/api/funkwhale_api/federation/utils.py b/api/funkwhale_api/federation/utils.py index 71f227464..d02c8bf68 100644 --- a/api/funkwhale_api/federation/utils.py +++ b/api/funkwhale_api/federation/utils.py @@ -1,5 +1,11 @@ +import unicodedata +import re from django.conf import settings +from funkwhale_api.common import session + +from . import signing + def full_url(path): """ @@ -32,3 +38,53 @@ def clean_wsgi_headers(raw_headers): cleaned[cleaned_header] = value return cleaned + + +def slugify_username(username): + """ + Given a username such as "hello M. world", returns a username + suitable for federation purpose (hello_M_world). + + Preserves the original case. + + Code is borrowed from django's slugify function. + """ + + value = str(username) + value = ( + unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") + ) + value = re.sub(r"[^\w\s-]", "", value).strip() + return re.sub(r"[-\s]+", "_", value) + + +def retrieve(fid, actor=None, serializer_class=None, queryset=None): + if queryset: + try: + # queryset can also be a Model class + existing = queryset.filter(fid=fid).first() + except AttributeError: + existing = queryset.objects.filter(fid=fid).first() + if existing: + return existing + + auth = ( + None if not actor else signing.get_auth(actor.private_key, actor.private_key_id) + ) + response = session.get_session().get( + fid, + auth=auth, + timeout=5, + verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, + headers={ + "Accept": "application/activity+json", + "Content-Type": "application/activity+json", + }, + ) + response.raise_for_status() + data = response.json() + if not serializer_class: + return data + serializer = serializer_class(data=data) + serializer.is_valid(raise_exception=True) + return serializer.save() diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index 29f566309..a12d5e5b5 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -1,28 +1,14 @@ from django import forms from django.core import paginator -from django.db import transaction -from django.http import HttpResponse, Http404 +from django.http import HttpResponse from django.urls import reverse -from rest_framework import mixins, response, viewsets +from rest_framework import exceptions, mixins, response, viewsets from rest_framework.decorators import detail_route, list_route from funkwhale_api.common import preferences from funkwhale_api.music import models as music_models -from funkwhale_api.users.permissions import HasUserPermission -from . import ( - actors, - authentication, - filters, - library, - models, - permissions, - renderers, - serializers, - tasks, - utils, - webfinger, -) +from . import activity, authentication, models, renderers, serializers, utils, webfinger class FederationMixin(object): @@ -32,9 +18,24 @@ class FederationMixin(object): return super().dispatch(request, *args, **kwargs) +class SharedViewSet(FederationMixin, viewsets.GenericViewSet): + permission_classes = [] + authentication_classes = [authentication.SignatureAuthentication] + renderer_classes = [renderers.ActivityPubRenderer] + + @list_route(methods=["post"]) + def inbox(self, request, *args, **kwargs): + if request.method.lower() == "post" and request.actor is None: + raise exceptions.AuthenticationFailed( + "You need a valid signature to send an activity" + ) + if request.method.lower() == "post": + activity.receive(activity=request.data, on_behalf_of=request.actor) + return response.Response({}, status=200) + + class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): - lookup_field = "user__username" - lookup_value_regex = ".*" + lookup_field = "preferred_username" authentication_classes = [authentication.SignatureAuthentication] permission_classes = [] renderer_classes = [renderers.ActivityPubRenderer] @@ -43,52 +44,29 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV @detail_route(methods=["get", "post"]) def inbox(self, request, *args, **kwargs): + if request.method.lower() == "post" and request.actor is None: + raise exceptions.AuthenticationFailed( + "You need a valid signature to send an activity" + ) + if request.method.lower() == "post": + activity.receive(activity=request.data, on_behalf_of=request.actor) return response.Response({}, status=200) @detail_route(methods=["get", "post"]) def outbox(self, request, *args, **kwargs): return response.Response({}, status=200) + @detail_route(methods=["get"]) + def followers(self, request, *args, **kwargs): + self.get_object() + # XXX to implement + return response.Response({}) -class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet): - 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"]] - except KeyError: - raise Http404 - - def retrieve(self, request, *args, **kwargs): - system_actor = self.get_object() - actor = system_actor.get_actor_instance() - data = actor.system_conf.serialize() - return response.Response(data, status=200) - - @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())) - - try: - handler(request.data, actor=request.actor) - except NotImplementedError: - return response.Response(status=405) - return response.Response({}, status=200) - - @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())) - try: - handler(request.data, actor=request.actor) - except NotImplementedError: - return response.Response(status=405) - return response.Response({}, status=200) + @detail_route(methods=["get"]) + def following(self, request, *args, **kwargs): + self.get_object() + # XXX to implement + return response.Response({}) class WellKnownViewSet(viewsets.GenericViewSet): @@ -132,56 +110,69 @@ class WellKnownViewSet(viewsets.GenericViewSet): def handler_acct(self, clean_result): username, hostname = clean_result - if username in actors.SYSTEM_ACTORS: - actor = actors.SYSTEM_ACTORS[username].get_actor_instance() - else: - try: - actor = models.Actor.objects.local().get(user__username=username) - except models.Actor.DoesNotExist: - raise forms.ValidationError("Invalid username") + try: + actor = models.Actor.objects.local().get(preferred_username=username) + except models.Actor.DoesNotExist: + raise forms.ValidationError("Invalid username") return serializers.ActorWebfingerSerializer(actor).data -class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet): - authentication_classes = [authentication.SignatureAuthentication] - permission_classes = [permissions.LibraryFollower] - renderer_classes = [renderers.ActivityPubRenderer] +def has_library_access(request, library): + if library.privacy_level == "everyone": + return True + if request.user.is_authenticated and request.user.is_superuser: + return True - def list(self, request, *args, **kwargs): + try: + actor = request.actor + except AttributeError: + return False + + return library.received_follows.filter(actor=actor, approved=True).exists() + + +class MusicLibraryViewSet( + FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + authentication_classes = [authentication.SignatureAuthentication] + permission_classes = [] + renderer_classes = [renderers.ActivityPubRenderer] + serializer_class = serializers.LibrarySerializer + queryset = music_models.Library.objects.all().select_related("actor") + lookup_field = "uuid" + + def retrieve(self, request, *args, **kwargs): + lb = self.get_object() + + conf = { + "id": lb.get_federation_id(), + "actor": lb.actor, + "name": lb.name, + "summary": lb.description, + "items": lb.uploads.for_federation().order_by("-creation_date"), + "item_serializer": serializers.UploadSerializer, + } 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, - } - serializer = serializers.PaginatedCollectionSerializer(conf) + serializer = serializers.LibrarySerializer(lb) data = serializer.data else: + # if actor is requesting a specific page, we ensure library is public + # or readable by the actor + if not has_library_access(request, lb): + raise exceptions.AuthenticationFailed( + "You do not have access to this library" + ) try: page_number = int(page) except Exception: return response.Response({"page": ["Invalid page number"]}, status=400) - p = paginator.Paginator( - qs, preferences.get("federation__collection_page_size") - ) + conf["page_size"] = preferences.get("federation__collection_page_size") + p = paginator.Paginator(conf["items"], conf["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, - } + conf["page"] = page serializer = serializers.CollectionPageSerializer(conf) data = serializer.data except paginator.EmptyPage: @@ -189,115 +180,48 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet): return response.Response(data) - -class LibraryViewSet( - 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" - filter_class = filters.LibraryFilter - serializer_class = serializers.APILibrarySerializer - ordering_fields = ( - "id", - "creation_date", - "fetched_date", - "actor__domain", - "tracks_count", - ) - - @list_route(methods=["get"]) - def fetch(self, request, *args, **kwargs): - account = request.GET.get("account") - if not account: - 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"]) - def scan(self, request, *args, **kwargs): - library = self.get_object() - 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") - ) - return response.Response({"task": result.id}) - - @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") - ) - 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)} - return response.Response(data) - - @list_route(methods=["get", "patch"]) + @detail_route(methods=["get"]) def followers(self, request, *args, **kwargs): - 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) - - 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)} - return response.Response(data) - - @transaction.atomic - def create(self, request, *args, **kwargs): - serializer = serializers.APILibraryCreateSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - serializer.save() - return response.Response(serializer.data, status=201) + self.get_object() + # XXX Implement this + return response.Response({}) -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") - ) - filter_class = filters.LibraryTrackFilter - serializer_class = serializers.APILibraryTrackSerializer - ordering_fields = ( - "id", - "artist_name", - "title", - "album_title", - "creation_date", - "modification_date", - "fetched_date", - "published_date", - ) +class MusicUploadViewSet( + FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + authentication_classes = [authentication.SignatureAuthentication] + permission_classes = [] + renderer_classes = [renderers.ActivityPubRenderer] + queryset = music_models.Upload.objects.none() + lookup_field = "uuid" - @list_route(methods=["post"]) - def action(self, request, *args, **kwargs): - queryset = models.LibraryTrack.objects.filter(local_track_file__isnull=True) - serializer = serializers.LibraryTrackActionSerializer( - request.data, queryset=queryset, context={"submitted_by": request.user} - ) - serializer.is_valid(raise_exception=True) - result = serializer.save() - return response.Response(result, status=200) + +class MusicArtistViewSet( + FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + authentication_classes = [authentication.SignatureAuthentication] + permission_classes = [] + renderer_classes = [renderers.ActivityPubRenderer] + queryset = music_models.Artist.objects.none() + lookup_field = "uuid" + + +class MusicAlbumViewSet( + FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + authentication_classes = [authentication.SignatureAuthentication] + permission_classes = [] + renderer_classes = [renderers.ActivityPubRenderer] + queryset = music_models.Album.objects.none() + lookup_field = "uuid" + + +class MusicTrackViewSet( + FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + authentication_classes = [authentication.SignatureAuthentication] + permission_classes = [] + renderer_classes = [renderers.ActivityPubRenderer] + queryset = music_models.Track.objects.none() + lookup_field = "uuid" diff --git a/api/funkwhale_api/history/admin.py b/api/funkwhale_api/history/admin.py index cbc7f89dd..6aac94d0a 100644 --- a/api/funkwhale_api/history/admin.py +++ b/api/funkwhale_api/history/admin.py @@ -1,4 +1,4 @@ -from django.contrib import admin +from funkwhale_api.common import admin from . import models diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py index 6c7ef3991..ec3b4f3c0 100644 --- a/api/funkwhale_api/history/views.py +++ b/api/funkwhale_api/history/views.py @@ -1,9 +1,12 @@ from rest_framework import mixins, viewsets from rest_framework.permissions import IsAuthenticatedOrReadOnly +from django.db.models import Prefetch + from funkwhale_api.activity import record from funkwhale_api.common import fields, permissions - +from funkwhale_api.music.models import Track +from funkwhale_api.music import utils as music_utils from . import models, serializers @@ -15,11 +18,7 @@ class ListeningViewSet( ): serializer_class = serializers.ListeningSerializer - queryset = ( - models.Listening.objects.all() - .select_related("track__artist", "track__album__artist", "user") - .prefetch_related("track__files") - ) + queryset = models.Listening.objects.all().select_related("user") permission_classes = [ permissions.ConditionalAuthentication, permissions.OwnerPermission, @@ -39,9 +38,13 @@ class ListeningViewSet( def get_queryset(self): queryset = super().get_queryset() - return queryset.filter( + queryset = queryset.filter( fields.privacy_level_query(self.request.user, "user__privacy_level") ) + tracks = Track.objects.annotate_playable_by_actor( + music_utils.get_actor_from_request(self.request) + ).select_related("artist", "album__artist") + return queryset.prefetch_related(Prefetch("track", queryset=tracks)) def get_serializer_context(self): context = super().get_serializer_context() diff --git a/api/funkwhale_api/instance/stats.py b/api/funkwhale_api/instance/stats.py index 061aade75..0cb1b9796 100644 --- a/api/funkwhale_api/instance/stats.py +++ b/api/funkwhale_api/instance/stats.py @@ -43,7 +43,7 @@ def get_artists(): def get_music_duration(): - seconds = models.TrackFile.objects.aggregate(d=Sum("duration"))["d"] + seconds = models.Upload.objects.aggregate(d=Sum("duration"))["d"] if seconds: return seconds / 3600 return 0 diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index 8098ef1a2..7f6e328db 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -2,11 +2,10 @@ from django_filters import rest_framework as filters from funkwhale_api.common import fields from funkwhale_api.music import models as music_models -from funkwhale_api.requests import models as requests_models from funkwhale_api.users import models as users_models -class ManageTrackFileFilterSet(filters.FilterSet): +class ManageUploadFilterSet(filters.FilterSet): q = fields.SearchFilter( search_fields=[ "track__title", @@ -17,8 +16,8 @@ class ManageTrackFileFilterSet(filters.FilterSet): ) class Meta: - model = music_models.TrackFile - fields = ["q", "track__album", "track__artist", "track", "library_track"] + model = music_models.Upload + fields = ["q", "track__album", "track__artist", "track"] class ManageUserFilterSet(filters.FilterSet): @@ -51,13 +50,3 @@ class ManageInvitationFilterSet(filters.FilterSet): if value is None: return queryset return queryset.open(value) - - -class ManageImportRequestFilterSet(filters.FilterSet): - q = fields.SearchFilter( - search_fields=["user__username", "albums", "artist_name", "comment"] - ) - - class Meta: - model = requests_models.ImportRequest - fields = ["q", "status"] diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index 42585d6a7..9b5e24f66 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -1,23 +1,22 @@ from django.db import transaction -from django.utils import timezone + from rest_framework import serializers from funkwhale_api.common import serializers as common_serializers from funkwhale_api.music import models as music_models -from funkwhale_api.requests import models as requests_models from funkwhale_api.users import models as users_models from . import filters -class ManageTrackFileArtistSerializer(serializers.ModelSerializer): +class ManageUploadArtistSerializer(serializers.ModelSerializer): class Meta: model = music_models.Artist fields = ["id", "mbid", "creation_date", "name"] -class ManageTrackFileAlbumSerializer(serializers.ModelSerializer): - artist = ManageTrackFileArtistSerializer() +class ManageUploadAlbumSerializer(serializers.ModelSerializer): + artist = ManageUploadArtistSerializer() class Meta: model = music_models.Album @@ -32,20 +31,20 @@ class ManageTrackFileAlbumSerializer(serializers.ModelSerializer): ) -class ManageTrackFileTrackSerializer(serializers.ModelSerializer): - artist = ManageTrackFileArtistSerializer() - album = ManageTrackFileAlbumSerializer() +class ManageUploadTrackSerializer(serializers.ModelSerializer): + artist = ManageUploadArtistSerializer() + album = ManageUploadAlbumSerializer() class Meta: model = music_models.Track fields = ("id", "mbid", "title", "album", "artist", "creation_date", "position") -class ManageTrackFileSerializer(serializers.ModelSerializer): - track = ManageTrackFileTrackSerializer() +class ManageUploadSerializer(serializers.ModelSerializer): + track = ManageUploadTrackSerializer() class Meta: - model = music_models.TrackFile + model = music_models.Upload fields = ( "id", "path", @@ -59,13 +58,12 @@ class ManageTrackFileSerializer(serializers.ModelSerializer): "bitrate", "size", "path", - "library_track", ) -class ManageTrackFileActionSerializer(common_serializers.ActionSerializer): +class ManageUploadActionSerializer(common_serializers.ActionSerializer): actions = [common_serializers.Action("delete", allow_all=False)] - filterset_class = filters.ManageTrackFileFilterSet + filterset_class = filters.ManageUploadFilterSet @transaction.atomic def handle_delete(self, objects): @@ -94,11 +92,13 @@ class ManageUserSimpleSerializer(serializers.ModelSerializer): "date_joined", "last_activity", "privacy_level", + "upload_quota", ) class ManageUserSerializer(serializers.ModelSerializer): permissions = PermissionsSerializer(source="*") + upload_quota = serializers.IntegerField(allow_null=True) class Meta: model = users_models.User @@ -114,6 +114,7 @@ class ManageUserSerializer(serializers.ModelSerializer): "last_activity", "permissions", "privacy_level", + "upload_quota", ) read_only_fields = [ "id", @@ -167,69 +168,3 @@ class ManageInvitationActionSerializer(common_serializers.ActionSerializer): @transaction.atomic def handle_delete(self, objects): return objects.delete() - - -class ManageImportRequestSerializer(serializers.ModelSerializer): - user = ManageUserSimpleSerializer(required=False) - - class Meta: - model = requests_models.ImportRequest - fields = [ - "id", - "status", - "creation_date", - "imported_date", - "user", - "albums", - "artist_name", - "comment", - ] - read_only_fields = [ - "id", - "status", - "creation_date", - "imported_date", - "user", - "albums", - "artist_name", - "comment", - ] - - def validate_code(self, value): - if not value: - return value - if users_models.Invitation.objects.filter(code__iexact=value).exists(): - raise serializers.ValidationError( - "An invitation with this code already exists" - ) - return value - - -class ManageImportRequestActionSerializer(common_serializers.ActionSerializer): - actions = [ - common_serializers.Action( - "mark_closed", - allow_all=True, - qs_filter=lambda qs: qs.filter(status__in=["pending", "accepted"]), - ), - common_serializers.Action( - "mark_imported", - allow_all=True, - qs_filter=lambda qs: qs.filter(status__in=["pending", "accepted"]), - ), - common_serializers.Action("delete", allow_all=False), - ] - filterset_class = filters.ManageImportRequestFilterSet - - @transaction.atomic - def handle_delete(self, objects): - return objects.delete() - - @transaction.atomic - def handle_mark_closed(self, objects): - return objects.update(status="closed") - - @transaction.atomic - def handle_mark_imported(self, objects): - now = timezone.now() - return objects.update(status="imported", imported_date=now) diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py index 8285ade06..9f5503978 100644 --- a/api/funkwhale_api/manage/urls.py +++ b/api/funkwhale_api/manage/urls.py @@ -4,11 +4,7 @@ from rest_framework import routers from . import views library_router = routers.SimpleRouter() -library_router.register(r"track-files", views.ManageTrackFileViewSet, "track-files") -requests_router = routers.SimpleRouter() -requests_router.register( - r"import-requests", views.ManageImportRequestViewSet, "import-requests" -) +library_router.register(r"uploads", views.ManageUploadViewSet, "uploads") users_router = routers.SimpleRouter() users_router.register(r"users", views.ManageUserViewSet, "users") users_router.register(r"invitations", views.ManageInvitationViewSet, "invitations") @@ -16,7 +12,4 @@ users_router.register(r"invitations", views.ManageInvitationViewSet, "invitation urlpatterns = [ url(r"^library/", include((library_router.urls, "instance"), namespace="library")), url(r"^users/", include((users_router.urls, "instance"), namespace="users")), - url( - r"^requests/", include((requests_router.urls, "instance"), namespace="requests") - ), ] diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index 89d2afe45..bfd5b2ef2 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -3,23 +3,22 @@ from rest_framework.decorators import list_route from funkwhale_api.common import preferences from funkwhale_api.music import models as music_models -from funkwhale_api.requests import models as requests_models from funkwhale_api.users import models as users_models from funkwhale_api.users.permissions import HasUserPermission from . import filters, serializers -class ManageTrackFileViewSet( +class ManageUploadViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet ): queryset = ( - music_models.TrackFile.objects.all() - .select_related("track__artist", "track__album__artist", "library_track") + music_models.Upload.objects.all() + .select_related("track__artist", "track__album__artist") .order_by("-id") ) - serializer_class = serializers.ManageTrackFileSerializer - filter_class = filters.ManageTrackFileFilterSet + serializer_class = serializers.ManageUploadSerializer + filter_class = filters.ManageUploadFilterSet permission_classes = (HasUserPermission,) required_permissions = ["library"] ordering_fields = [ @@ -35,7 +34,7 @@ class ManageTrackFileViewSet( @list_route(methods=["post"]) def action(self, request, *args, **kwargs): queryset = self.get_queryset() - serializer = serializers.ManageTrackFileActionSerializer( + serializer = serializers.ManageUploadActionSerializer( request.data, queryset=queryset ) serializer.is_valid(raise_exception=True) @@ -93,31 +92,3 @@ class ManageInvitationViewSet( serializer.is_valid(raise_exception=True) result = serializer.save() return response.Response(result, status=200) - - -class ManageImportRequestViewSet( - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - viewsets.GenericViewSet, -): - queryset = ( - requests_models.ImportRequest.objects.all() - .order_by("-id") - .select_related("user") - ) - serializer_class = serializers.ManageImportRequestSerializer - filter_class = filters.ManageImportRequestFilterSet - permission_classes = (HasUserPermission,) - required_permissions = ["library"] - ordering_fields = ["creation_date", "imported_date"] - - @list_route(methods=["post"]) - def action(self, request, *args, **kwargs): - queryset = self.get_queryset() - serializer = serializers.ManageImportRequestActionSerializer( - request.data, queryset=queryset - ) - serializer.is_valid(raise_exception=True) - result = serializer.save() - return response.Response(result, status=200) diff --git a/api/funkwhale_api/music/admin.py b/api/funkwhale_api/music/admin.py index a5775acd6..8f9768857 100644 --- a/api/funkwhale_api/music/admin.py +++ b/api/funkwhale_api/music/admin.py @@ -1,4 +1,4 @@ -from django.contrib import admin +from funkwhale_api.common import admin from . import models @@ -33,8 +33,8 @@ class ImportBatchAdmin(admin.ModelAdmin): @admin.register(models.ImportJob) class ImportJobAdmin(admin.ModelAdmin): - list_display = ["source", "batch", "track_file", "status", "mbid"] - list_select_related = ["track_file", "batch"] + list_display = ["source", "batch", "upload", "status", "mbid"] + list_select_related = ["upload", "batch"] search_fields = ["source", "batch__pk", "mbid"] list_filter = ["status"] @@ -55,8 +55,8 @@ class LyricsAdmin(admin.ModelAdmin): list_filter = ["work__language"] -@admin.register(models.TrackFile) -class TrackFileAdmin(admin.ModelAdmin): +@admin.register(models.Upload) +class UploadAdmin(admin.ModelAdmin): list_display = [ "track", "audio_file", @@ -65,6 +65,7 @@ class TrackFileAdmin(admin.ModelAdmin): "mimetype", "size", "bitrate", + "import_status", ] list_select_related = ["track"] search_fields = [ @@ -74,4 +75,40 @@ class TrackFileAdmin(admin.ModelAdmin): "track__album__title", "track__artist__name", ] - list_filter = ["mimetype"] + list_filter = ["mimetype", "import_status", "library__privacy_level"] + + +def launch_scan(modeladmin, request, queryset): + for library in queryset: + library.schedule_scan(actor=request.user.actor, force=True) + + +launch_scan.short_description = "Launch scan" + + +@admin.register(models.Library) +class LibraryAdmin(admin.ModelAdmin): + list_display = ["id", "name", "actor", "uuid", "privacy_level", "creation_date"] + list_select_related = True + search_fields = ["actor__username", "name", "description"] + list_filter = ["privacy_level"] + actions = [launch_scan] + + +@admin.register(models.LibraryScan) +class LibraryScanAdmin(admin.ModelAdmin): + list_display = [ + "id", + "library", + "actor", + "status", + "creation_date", + "modification_date", + "status", + "total_files", + "processed_files", + "errored_files", + ] + list_select_related = True + search_fields = ["actor__username", "library__name"] + list_filter = ["status"] diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py index 9bcc4350f..9571f9785 100644 --- a/api/funkwhale_api/music/factories.py +++ b/api/funkwhale_api/music/factories.py @@ -3,8 +3,9 @@ import os import factory from funkwhale_api.factories import ManyToManyFromList, registry -from funkwhale_api.federation.factories import LibraryTrackFactory -from funkwhale_api.users.factories import UserFactory +from funkwhale_api.federation import factories as federation_factories +from funkwhale_api.users import factories as users_factories + SAMPLES_PATH = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), @@ -13,10 +14,28 @@ SAMPLES_PATH = os.path.join( ) +def playable_factory(field): + @factory.post_generation + def inner(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + UploadFactory( + library__privacy_level="everyone", + import_status="finished", + **{field: self} + ) + + return inner + + @registry.register class ArtistFactory(factory.django.DjangoModelFactory): name = factory.Faker("name") mbid = factory.Faker("uuid4") + fid = factory.Faker("federation_url") + playable = playable_factory("track__album__artist") class Meta: model = "music.Artist" @@ -30,6 +49,8 @@ class AlbumFactory(factory.django.DjangoModelFactory): cover = factory.django.ImageField() artist = factory.SubFactory(ArtistFactory) release_group_id = factory.Faker("uuid4") + fid = factory.Faker("federation_url") + playable = playable_factory("track__album") class Meta: model = "music.Album" @@ -37,20 +58,24 @@ class AlbumFactory(factory.django.DjangoModelFactory): @registry.register class TrackFactory(factory.django.DjangoModelFactory): + fid = factory.Faker("federation_url") title = factory.Faker("sentence", nb_words=3) mbid = factory.Faker("uuid4") album = factory.SubFactory(AlbumFactory) artist = factory.SelfAttribute("album.artist") position = 1 tags = ManyToManyFromList("tags") + playable = playable_factory("track") class Meta: model = "music.Track" @registry.register -class TrackFileFactory(factory.django.DjangoModelFactory): +class UploadFactory(factory.django.DjangoModelFactory): + fid = factory.Faker("federation_url") track = factory.SubFactory(TrackFactory) + library = factory.SubFactory(federation_factories.MusicLibraryFactory) audio_file = factory.django.FileField( from_path=os.path.join(SAMPLES_PATH, "test.ogg") ) @@ -58,69 +83,18 @@ class TrackFileFactory(factory.django.DjangoModelFactory): bitrate = None size = None duration = None + mimetype = "audio/ogg" class Meta: - model = "music.TrackFile" + model = "music.Upload" class Params: 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), + playable = factory.Trait( + import_status="finished", library__privacy_level="everyone" ) -@registry.register -class ImportBatchFactory(factory.django.DjangoModelFactory): - submitted_by = factory.SubFactory(UserFactory) - - class Meta: - model = "music.ImportBatch" - - class Params: - 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") - replace_if_duplicate = False - - class Meta: - model = "music.ImportJob" - - class Params: - federation = factory.Trait( - mbid=None, - library_track=factory.SubFactory(LibraryTrackFactory), - 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) - with_audio_file = factory.Trait( - status="finished", - audio_file=factory.django.FileField( - from_path=os.path.join(SAMPLES_PATH, "test.ogg") - ), - ) - - -@registry.register(name="music.FileImportJob") -class FileImportJobFactory(ImportJobFactory): - source = "file://" - mbid = None - audio_file = factory.django.FileField( - from_path=os.path.join(SAMPLES_PATH, "test.ogg") - ) - - @registry.register class WorkFactory(factory.django.DjangoModelFactory): mbid = factory.Faker("uuid4") @@ -149,3 +123,24 @@ class TagFactory(factory.django.DjangoModelFactory): class Meta: model = "taggit.Tag" + + +# XXX To remove + + +class ImportBatchFactory(factory.django.DjangoModelFactory): + submitted_by = factory.SubFactory(users_factories.UserFactory) + + class Meta: + model = "music.ImportBatch" + + +@registry.register +class ImportJobFactory(factory.django.DjangoModelFactory): + batch = factory.SubFactory(ImportBatchFactory) + source = factory.Faker("url") + mbid = factory.Faker("uuid4") + replace_if_duplicate = False + + class Meta: + model = "music.ImportJob" diff --git a/api/funkwhale_api/music/fake_data.py b/api/funkwhale_api/music/fake_data.py index e5fd65d8e..4264947ca 100644 --- a/api/funkwhale_api/music/fake_data.py +++ b/api/funkwhale_api/music/fake_data.py @@ -14,7 +14,7 @@ def create_data(count=25): artist=artist, size=random.randint(1, 5) ) for album in albums: - factories.TrackFileFactory.create_batch( + factories.UploadFactory.create_batch( track__album=album, size=random.randint(3, 18) ) diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index 87537b675..a0635368d 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -1,85 +1,97 @@ -from django.db.models import Count from django_filters import rest_framework as filters from funkwhale_api.common import fields +from funkwhale_api.common import search from . import models +from . import utils class ArtistFilter(filters.FilterSet): q = fields.SearchFilter(search_fields=["name"]) - listenable = filters.BooleanFilter(name="_", method="filter_listenable") + playable = filters.BooleanFilter(name="_", method="filter_playable") class Meta: model = models.Artist fields = { "name": ["exact", "iexact", "startswith", "icontains"], - "listenable": "exact", + "playable": "exact", } - def filter_listenable(self, queryset, name, value): - queryset = queryset.annotate(files_count=Count("albums__tracks__files")) - if value: - return queryset.filter(files_count__gt=0) - else: - return queryset.filter(files_count=0) + def filter_playable(self, queryset, name, value): + actor = utils.get_actor_from_request(self.request) + return queryset.playable_by(actor, value) class TrackFilter(filters.FilterSet): q = fields.SearchFilter(search_fields=["title", "album__title", "artist__name"]) - listenable = filters.BooleanFilter(name="_", method="filter_listenable") + playable = filters.BooleanFilter(name="_", method="filter_playable") class Meta: model = models.Track fields = { "title": ["exact", "iexact", "startswith", "icontains"], - "listenable": ["exact"], + "playable": ["exact"], "artist": ["exact"], "album": ["exact"], } - def filter_listenable(self, queryset, name, value): - queryset = queryset.annotate(files_count=Count("files")) - if value: - return queryset.filter(files_count__gt=0) - else: - return queryset.filter(files_count=0) + def filter_playable(self, queryset, name, value): + actor = utils.get_actor_from_request(self.request) + return queryset.playable_by(actor, value) -class ImportBatchFilter(filters.FilterSet): - q = fields.SearchFilter(search_fields=["submitted_by__username", "source"]) +class UploadFilter(filters.FilterSet): + library = filters.CharFilter("library__uuid") + track = filters.UUIDFilter("track__uuid") + track_artist = filters.UUIDFilter("track__artist__uuid") + album_artist = filters.UUIDFilter("track__album__artist__uuid") + library = filters.UUIDFilter("library__uuid") + playable = filters.BooleanFilter(name="_", method="filter_playable") + q = fields.SmartSearchFilter( + config=search.SearchConfig( + search_fields={ + "track_artist": {"to": "track__artist__name"}, + "album_artist": {"to": "track__album__artist__name"}, + "album": {"to": "track__album__title"}, + "title": {"to": "track__title"}, + }, + filter_fields={ + "artist": {"to": "track__artist__name__iexact"}, + "mimetype": {"to": "mimetype"}, + "album": {"to": "track__album__title__iexact"}, + "title": {"to": "track__title__iexact"}, + "status": {"to": "import_status"}, + }, + ) + ) class Meta: - model = models.ImportBatch - fields = {"status": ["exact"], "source": ["exact"], "submitted_by": ["exact"]} + model = models.Upload + fields = [ + "playable", + "import_status", + "mimetype", + "track", + "track_artist", + "album_artist", + "library", + "import_reference", + ] - -class ImportJobFilter(filters.FilterSet): - 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"], - } + def filter_playable(self, queryset, name, value): + actor = utils.get_actor_from_request(self.request) + return queryset.playable_by(actor, value) class AlbumFilter(filters.FilterSet): - listenable = filters.BooleanFilter(name="_", method="filter_listenable") + playable = filters.BooleanFilter(name="_", method="filter_playable") q = fields.SearchFilter(search_fields=["title", "artist__name" "source"]) class Meta: model = models.Album - fields = ["listenable", "q", "artist"] + fields = ["playable", "q", "artist"] - def filter_listenable(self, queryset, name, value): - queryset = queryset.annotate(files_count=Count("tracks__files")) - if value: - return queryset.filter(files_count__gt=0) - else: - return queryset.filter(files_count=0) + def filter_playable(self, queryset, name, value): + actor = utils.get_actor_from_request(self.request) + return queryset.playable_by(actor, value) diff --git a/api/funkwhale_api/music/importers.py b/api/funkwhale_api/music/importers.py index fc4a98241..28763a495 100644 --- a/api/funkwhale_api/music/importers.py +++ b/api/funkwhale_api/music/importers.py @@ -15,7 +15,7 @@ class Importer(object): # let's validate data, just in case instance = self.model(**cleaned_data) exclude = EXCLUDE_VALIDATION.get(self.model.__name__, []) - instance.full_clean(exclude=["mbid", "uuid"] + exclude) + instance.full_clean(exclude=["mbid", "uuid", "fid", "from_activity"] + exclude) m = self.model.objects.update_or_create(mbid=mbid, defaults=cleaned_data)[0] for hook in import_hooks: hook(m, cleaned_data, raw_data) diff --git a/api/funkwhale_api/music/management/commands/fix_track_files.py b/api/funkwhale_api/music/management/commands/fix_uploads.py similarity index 76% rename from api/funkwhale_api/music/management/commands/fix_track_files.py rename to api/funkwhale_api/music/management/commands/fix_uploads.py index c61972db8..94f8dd21c 100644 --- a/api/funkwhale_api/music/management/commands/fix_track_files.py +++ b/api/funkwhale_api/music/management/commands/fix_uploads.py @@ -27,9 +27,9 @@ class Command(BaseCommand): @transaction.atomic def fix_mimetypes(self, dry_run, **kwargs): self.stdout.write("Fixing missing mimetypes...") - matching = models.TrackFile.objects.filter( - source__startswith="file://" - ).exclude(mimetype__startswith="audio/") + matching = models.Upload.objects.filter(source__startswith="file://").exclude( + mimetype__startswith="audio/" + ) self.stdout.write( "[mimetypes] {} entries found with bad or no mimetype".format( matching.count() @@ -48,7 +48,7 @@ class Command(BaseCommand): def fix_file_data(self, dry_run, **kwargs): self.stdout.write("Fixing missing bitrate or length...") - matching = models.TrackFile.objects.filter( + matching = models.Upload.objects.filter( Q(bitrate__isnull=True) | Q(duration__isnull=True) ) total = matching.count() @@ -57,41 +57,41 @@ class Command(BaseCommand): ) if dry_run: return - for i, tf in enumerate(matching.only("audio_file")): + for i, upload 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, upload.pk) ) try: - audio_file = tf.get_audio_file() + audio_file = upload.get_audio_file() if audio_file: data = utils.get_audio_file_data(audio_file) - tf.bitrate = data["bitrate"] - tf.duration = data["length"] - tf.save(update_fields=["duration", "bitrate"]) + upload.bitrate = data["bitrate"] + upload.duration = data["length"] + upload.save(update_fields=["duration", "bitrate"]) else: 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(upload.pk, str(e)) ) def fix_file_size(self, dry_run, **kwargs): self.stdout.write("Fixing missing size...") - matching = models.TrackFile.objects.filter(size__isnull=True) + matching = models.Upload.objects.filter(size__isnull=True) total = matching.count() 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, upload in enumerate(matching.only("size")): self.stdout.write( - "[size] {}/{} fixing file #{}".format(i + 1, total, tf.pk) + "[size] {}/{} fixing file #{}".format(i + 1, total, upload.pk) ) try: - tf.size = tf.get_file_size() - tf.save(update_fields=["size"]) + upload.size = upload.get_file_size() + upload.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(upload.pk, str(e)) ) diff --git a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py b/api/funkwhale_api/music/management/commands/import_files.py similarity index 61% rename from api/funkwhale_api/providers/audiofile/management/commands/import_files.py rename to api/funkwhale_api/music/management/commands/import_files.py index 625f9c2f0..af5d2cee2 100644 --- a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py +++ b/api/funkwhale_api/music/management/commands/import_files.py @@ -1,18 +1,29 @@ import glob import os +import urllib.parse from django.conf import settings from django.core.files import File from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone from funkwhale_api.music import models, tasks -from funkwhale_api.users.models import User class Command(BaseCommand): help = "Import audio files mathinc given glob pattern" def add_arguments(self, parser): + parser.add_argument( + "library_id", + type=str, + help=( + "A local library identifier where the files should be imported. " + "You can use the full uuid such as e29c5be9-6da3-4d92-b40b-4970edd3ee4b " + "or only a small portion of it, starting from the beginning, such as " + "e29c5be9" + ), + ) parser.add_argument("path", nargs="+", type=str) parser.add_argument( "--recursive", @@ -29,7 +40,7 @@ class Command(BaseCommand): parser.add_argument( "--async", action="store_true", - dest="async", + dest="async_", default=False, help="Will launch celery tasks for each file to import instead of doing it synchronously and block the CLI", ) @@ -66,6 +77,40 @@ class Command(BaseCommand): "with their newest version." ), ) + parser.add_argument( + "--outbox", + action="store_true", + dest="outbox", + default=False, + help=( + "Use this flag to notify library followers of newly imported files. " + "You'll likely want to keep this disabled for CLI imports, especially if" + "you plan to import hundreds or thousands of files, as it will cause a lot " + "of overhead on your server and on servers you are federating with." + ), + ) + + parser.add_argument( + "--broadcast", + action="store_true", + dest="broadcast", + default=False, + help=( + "Use this flag to enable realtime updates about the import in the UI. " + "This causes some overhead, so it's disabled by default." + ), + ) + + parser.add_argument( + "--reference", + action="store", + dest="reference", + default=None, + help=( + "A custom reference for the import. Leave this empty to have a random " + "reference being generated for you." + ), + ) parser.add_argument( "--noinput", "--no-input", @@ -77,14 +122,22 @@ class Command(BaseCommand): def handle(self, *args, **options): glob_kwargs = {} matching = [] + + try: + library = models.Library.objects.select_related("actor__user").get( + uuid__startswith=options["library_id"] + ) + except models.Library.DoesNotExist: + raise CommandError("Invalid library id") + + if not library.actor.get_user(): + raise CommandError("Library {} is not a local library".format(library.uuid)) + if options["recursive"]: glob_kwargs["recursive"] = True - try: - for import_path in options["path"]: - matching += glob.glob(import_path, **glob_kwargs) - raw_matching = sorted(list(set(matching))) - except TypeError: - raise Exception("You need Python 3.5 to use the --recursive flag") + for import_path in options["path"]: + matching += glob.glob(import_path, **glob_kwargs) + raw_matching = sorted(list(set(matching))) matching = [] for m in raw_matching: @@ -128,28 +181,12 @@ class Command(BaseCommand): if not matching: raise CommandError("No file matching pattern, aborting") - user = None - if options["username"]: - try: - user = User.objects.get(username=options["username"]) - except User.DoesNotExist: - raise CommandError("Invalid username") - else: - # we bind the import to the first registered superuser - try: - user = User.objects.filter(is_superuser=True).order_by("pk").first() - assert user is not None - except AssertionError: - raise CommandError( - "No superuser available, please provide a --username" - ) - if options["replace"]: filtered = {"initial": matching, "skipped": [], "new": matching} message = "- {} files to be replaced" import_paths = matching else: - filtered = self.filter_matching(matching) + filtered = self.filter_matching(matching, library) message = "- {} files already found in database" import_paths = filtered["new"] @@ -179,10 +216,26 @@ class Command(BaseCommand): ) if input("".join(message)) != "yes": raise CommandError("Import cancelled.") + reference = options["reference"] or "cli-{}".format(timezone.now().isoformat()) - batch, errors = self.do_import(import_paths, user=user, options=options) + import_url = "{}://{}/content/libraries/{}/upload?{}" + import_url = import_url.format( + settings.FUNKWHALE_PROTOCOL, + settings.FUNKWHALE_HOSTNAME, + str(library.uuid), + urllib.parse.urlencode([("import", reference)]), + ) + self.stdout.write( + "For details, please refer to import refrence '{}' or URL {}".format( + reference, import_url + ) + ) + + errors = self.do_import( + import_paths, library=library, reference=reference, options=options + ) message = "Successfully imported {} tracks" - if options["async"]: + if options["async_"]: message = "Successfully launched import for {} tracks" self.stdout.write(message.format(len(import_paths))) @@ -191,15 +244,18 @@ class Command(BaseCommand): for path, error in errors: self.stderr.write("- {}: {}".format(path, error)) + self.stdout.write( - "For details, please refer to import batch #{}".format(batch.pk) + "For details, please refer to import refrence '{}' or URL {}".format( + reference, import_url + ) ) - def filter_matching(self, matching): + def filter_matching(self, matching, library): sources = ["file://{}".format(p) for p in matching] # we skip reimport for path that are already found - # as a TrackFile.source - existing = models.TrackFile.objects.filter(source__in=sources) + # as a Upload.source + existing = library.uploads.filter(source__in=sources, import_status="finished") existing = existing.values_list("source", flat=True) existing = set([p.replace("file://", "", 1) for p in existing]) skipped = set(matching) & existing @@ -210,20 +266,27 @@ class Command(BaseCommand): } return result - def do_import(self, paths, user, options): + def do_import(self, paths, library, reference, options): message = "{i}/{total} Importing {path}..." - if options["async"]: + if options["async_"]: message = "{i}/{total} Launching import for {path}..." - # we create an import batch binded to the user - async_ = options["async"] - import_handler = tasks.import_job_run.delay if async_ else tasks.import_job_run - batch = user.imports.create(source="shell") + # we create an upload binded to the library + async_ = options["async_"] errors = [] for i, path in list(enumerate(paths)): try: self.stdout.write(message.format(path=path, i=i + 1, total=len(paths))) - self.import_file(path, batch, import_handler, options) + self.create_upload( + path, + reference, + library, + async_, + options["replace"], + options["in_place"], + options["outbox"], + options["broadcast"], + ) except Exception as e: if options["exit_on_failure"]: raise @@ -232,16 +295,36 @@ class Command(BaseCommand): ) self.stderr.write(m) errors.append((path, "{} {}".format(e.__class__.__name__, e))) - return batch, errors + return errors - def import_file(self, path, batch, import_handler, options): - job = batch.jobs.create( - source="file://" + path, replace_if_duplicate=options["replace"] - ) - if not options["in_place"]: + def create_upload( + self, + path, + reference, + library, + async_, + replace, + in_place, + dispatch_outbox, + broadcast, + ): + import_handler = tasks.process_upload.delay if async_ else tasks.process_upload + upload = models.Upload(library=library, import_reference=reference) + upload.source = "file://" + path + upload.import_metadata = { + "funkwhale": { + "config": { + "replace": replace, + "dispatch_outbox": dispatch_outbox, + "broadcast": broadcast, + } + } + } + if not in_place: name = os.path.basename(path) with open(path, "rb") as f: - job.audio_file.save(name, File(f)) + upload.audio_file.save(name, File(f), save=False) - job.save() - import_handler(import_job_id=job.pk, use_acoustid=False) + upload.save() + + import_handler(upload_id=upload.pk) diff --git a/api/funkwhale_api/music/metadata.py b/api/funkwhale_api/music/metadata.py index 9aa08820a..21daf2747 100644 --- a/api/funkwhale_api/music/metadata.py +++ b/api/funkwhale_api/music/metadata.py @@ -93,9 +93,9 @@ def convert_track_number(v): 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] + # sometimes, Picard leaves two uuids in the field, separated + # by a slash or a ; + value = value.split(";")[0].split("/")[0].strip() except (AttributeError, IndexError, TypeError): pass @@ -107,13 +107,42 @@ def get_date(value): return datetime.date(parsed.year, parsed.month, parsed.day) +def split_and_return_first(separator): + def inner(v): + return v.split(separator)[0].strip() + + return inner + + VALIDATION = { "musicbrainz_artistid": FirstUUIDField(), "musicbrainz_albumid": FirstUUIDField(), "musicbrainz_recordingid": FirstUUIDField(), + "musicbrainz_albumartistid": FirstUUIDField(), } CONF = { + "OggOpus": { + "getter": lambda f, k: f[k][0], + "fields": { + "track_number": { + "field": "TRACKNUMBER", + "to_application": convert_track_number, + }, + "title": {}, + "artist": {}, + "album_artist": { + "field": "albumartist", + "to_application": split_and_return_first(";"), + }, + "album": {}, + "date": {"field": "date", "to_application": get_date}, + "musicbrainz_albumid": {}, + "musicbrainz_artistid": {}, + "musicbrainz_albumartistid": {}, + "musicbrainz_recordingid": {"field": "musicbrainz_trackid"}, + }, + }, "OggVorbis": { "getter": lambda f, k: f[k][0], "fields": { @@ -123,10 +152,15 @@ CONF = { }, "title": {}, "artist": {}, + "album_artist": { + "field": "albumartist", + "to_application": split_and_return_first(";"), + }, "album": {}, "date": {"field": "date", "to_application": get_date}, "musicbrainz_albumid": {}, "musicbrainz_artistid": {}, + "musicbrainz_albumartistid": {}, "musicbrainz_recordingid": {"field": "musicbrainz_trackid"}, }, }, @@ -139,10 +173,12 @@ CONF = { }, "title": {}, "artist": {}, + "album_artist": {"field": "albumartist"}, "album": {}, "date": {"field": "date", "to_application": get_date}, "musicbrainz_albumid": {"field": "MusicBrainz Album Id"}, "musicbrainz_artistid": {"field": "MusicBrainz Artist Id"}, + "musicbrainz_albumartistid": {"field": "MusicBrainz Album Artist Id"}, "musicbrainz_recordingid": {"field": "MusicBrainz Track Id"}, }, }, @@ -153,10 +189,12 @@ CONF = { "track_number": {"field": "TRCK", "to_application": convert_track_number}, "title": {"field": "TIT2"}, "artist": {"field": "TPE1"}, + "album_artist": {"field": "TPE2"}, "album": {"field": "TALB"}, "date": {"field": "TDRC", "to_application": get_date}, "musicbrainz_albumid": {"field": "MusicBrainz Album Id"}, "musicbrainz_artistid": {"field": "MusicBrainz Artist Id"}, + "musicbrainz_albumartistid": {"field": "MusicBrainz Album Artist Id"}, "musicbrainz_recordingid": { "field": "UFID", "getter": get_mp3_recording_id, @@ -174,10 +212,12 @@ CONF = { }, "title": {}, "artist": {}, + "album_artist": {"field": "albumartist"}, "album": {}, "date": {"field": "date", "to_application": get_date}, "musicbrainz_albumid": {}, "musicbrainz_artistid": {}, + "musicbrainz_albumartistid": {}, "musicbrainz_recordingid": {"field": "musicbrainz_trackid"}, "test": {}, "pictures": {}, @@ -185,6 +225,19 @@ CONF = { }, } +ALL_FIELDS = [ + "track_number", + "title", + "artist", + "album_artist", + "album", + "date", + "musicbrainz_albumid", + "musicbrainz_artistid", + "musicbrainz_albumartistid", + "musicbrainz_recordingid", +] + class Metadata(object): def __init__(self, path): @@ -222,6 +275,20 @@ class Metadata(object): v = field.to_python(v) return v + def all(self): + """ + Return a dict containing all metadata of the file + """ + + data = {} + for field in ALL_FIELDS: + try: + data[field] = self.get(field, None) + except (TagNotFound, forms.ValidationError): + data[field] = None + + return data + def get_picture(self, picture_type="cover_front"): ptype = getattr(mutagen.id3.PictureType, picture_type.upper()) try: diff --git a/api/funkwhale_api/music/migrations/0028_importjob_replace_if_duplicate.py b/api/funkwhale_api/music/migrations/0028_importjob_replace_if_duplicate.py index d02a17ad2..1878ed020 100644 --- a/api/funkwhale_api/music/migrations/0028_importjob_replace_if_duplicate.py +++ b/api/funkwhale_api/music/migrations/0028_importjob_replace_if_duplicate.py @@ -5,14 +5,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0027_auto_20180515_1808'), - ] + dependencies = [("music", "0027_auto_20180515_1808")] operations = [ migrations.AddField( - model_name='importjob', - name='replace_if_duplicate', + model_name="importjob", + name="replace_if_duplicate", field=models.BooleanField(default=False), - ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0029_auto_20180807_1748.py b/api/funkwhale_api/music/migrations/0029_auto_20180807_1748.py new file mode 100644 index 000000000..9dec6ef35 --- /dev/null +++ b/api/funkwhale_api/music/migrations/0029_auto_20180807_1748.py @@ -0,0 +1,109 @@ +# Generated by Django 2.0.7 on 2018-08-07 17:48 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("federation", "0007_auto_20180807_1748"), + ("music", "0028_importjob_replace_if_duplicate"), + ] + + operations = [ + migrations.CreateModel( + name="Library", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("fid", models.URLField(db_index=True, max_length=500, unique=True)), + ("url", models.URLField(blank=True, max_length=500, null=True)), + ( + "uuid", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ("followers_url", models.URLField(max_length=500)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("name", models.CharField(max_length=100)), + ( + "description", + models.TextField(blank=True, max_length=5000, null=True), + ), + ( + "privacy_level", + models.CharField( + choices=[ + ("me", "Only me"), + ("instance", "Everyone on my instance, and my followers"), + ( + "everyone", + "Everyone, including people on other instances", + ), + ], + default="me", + max_length=25, + ), + ), + ( + "actor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="libraries", + to="federation.Actor", + ), + ), + ], + options={"abstract": False}, + ), + migrations.AddField( + model_name="importjob", + name="audio_file_size", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name="importbatch", + name="import_request", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="import_batches", + to="requests.ImportRequest", + ), + ), + migrations.AddField( + model_name="importbatch", + name="library", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="import_batches", + to="music.Library", + ), + ), + migrations.AddField( + model_name="trackfile", + name="library", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="files", + to="music.Library", + ), + ), + ] diff --git a/api/funkwhale_api/music/migrations/0030_auto_20180825_1411.py b/api/funkwhale_api/music/migrations/0030_auto_20180825_1411.py new file mode 100644 index 000000000..8c799dacb --- /dev/null +++ b/api/funkwhale_api/music/migrations/0030_auto_20180825_1411.py @@ -0,0 +1,152 @@ +# Generated by Django 2.0.8 on 2018-08-25 14:11 + +import django.contrib.postgres.fields.jsonb +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import funkwhale_api.music.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("federation", "0009_auto_20180822_1956"), + ("music", "0029_auto_20180807_1748"), + ] + + operations = [ + migrations.CreateModel( + name="LibraryScan", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("total_files", models.PositiveIntegerField(default=0)), + ("processed_files", models.PositiveIntegerField(default=0)), + ("errored_files", models.PositiveIntegerField(default=0)), + ("status", models.CharField(default="pending", max_length=25)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("modification_date", models.DateTimeField(blank=True, null=True)), + ( + "actor", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="federation.Actor", + ), + ), + ], + ), + migrations.RemoveField(model_name="trackfile", name="library_track"), + migrations.AddField( + model_name="library", + name="files_count", + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name="trackfile", + name="fid", + field=models.URLField(blank=True, max_length=500, null=True, unique=True), + ), + migrations.AddField( + model_name="trackfile", + name="import_date", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="trackfile", + name="import_details", + field=django.contrib.postgres.fields.jsonb.JSONField( + default=funkwhale_api.music.models.empty_dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + max_length=50000, + ), + ), + migrations.AddField( + model_name="trackfile", + name="import_metadata", + field=django.contrib.postgres.fields.jsonb.JSONField( + default=funkwhale_api.music.models.empty_dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + max_length=50000, + ), + ), + migrations.AddField( + model_name="trackfile", + name="import_reference", + field=models.CharField( + default=funkwhale_api.music.models.get_import_reference, max_length=50 + ), + ), + migrations.AddField( + model_name="trackfile", + name="import_status", + field=models.CharField( + choices=[ + ("pending", "Pending"), + ("finished", "Finished"), + ("errored", "Errored"), + ("skipped", "Skipped"), + ], + default="pending", + max_length=25, + ), + ), + migrations.AddField( + model_name="trackfile", + name="metadata", + field=django.contrib.postgres.fields.jsonb.JSONField( + default=funkwhale_api.music.models.empty_dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + max_length=50000, + ), + ), + migrations.AlterField( + model_name="album", + name="release_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name="trackfile", + name="audio_file", + field=models.FileField( + max_length=255, upload_to=funkwhale_api.music.models.get_file_path + ), + ), + migrations.AlterField( + model_name="trackfile", + name="source", + field=models.CharField(blank=True, max_length=500, null=True), + ), + migrations.AlterField( + model_name="trackfile", + name="track", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="files", + to="music.Track", + ), + ), + migrations.AddField( + model_name="libraryscan", + name="library", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="scans", + to="music.Library", + ), + ), + ] diff --git a/api/funkwhale_api/music/migrations/0031_auto_20180914_2007.py b/api/funkwhale_api/music/migrations/0031_auto_20180914_2007.py new file mode 100644 index 000000000..1e5590c9a --- /dev/null +++ b/api/funkwhale_api/music/migrations/0031_auto_20180914_2007.py @@ -0,0 +1,66 @@ +# Generated by Django 2.0.8 on 2018-09-14 20:07 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0011_auto_20180910_1902'), + ('music', '0030_auto_20180825_1411'), + ] + + operations = [ + migrations.AddField( + model_name='album', + name='fid', + field=models.URLField(db_index=True, max_length=500, null=True, unique=True), + ), + migrations.AddField( + model_name='album', + name='from_activity', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='federation.Activity'), + ), + migrations.AddField( + model_name='artist', + name='fid', + field=models.URLField(db_index=True, max_length=500, null=True, unique=True), + ), + migrations.AddField( + model_name='artist', + name='from_activity', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='federation.Activity'), + ), + migrations.AddField( + model_name='track', + name='fid', + field=models.URLField(db_index=True, max_length=500, null=True, unique=True), + ), + migrations.AddField( + model_name='track', + name='from_activity', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='federation.Activity'), + ), + migrations.AddField( + model_name='trackfile', + name='from_activity', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='federation.Activity'), + ), + migrations.AddField( + model_name='work', + name='fid', + field=models.URLField(db_index=True, max_length=500, null=True, unique=True), + ), + migrations.AddField( + model_name='work', + name='from_activity', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='federation.Activity'), + ), + migrations.AlterField( + model_name='trackfile', + name='modification_date', + field=models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ] diff --git a/api/funkwhale_api/music/migrations/0032_track_file_to_upload.py b/api/funkwhale_api/music/migrations/0032_track_file_to_upload.py new file mode 100644 index 000000000..282edf73a --- /dev/null +++ b/api/funkwhale_api/music/migrations/0032_track_file_to_upload.py @@ -0,0 +1,40 @@ +# Generated by Django 2.0.8 on 2018-09-21 16:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [("music", "0031_auto_20180914_2007")] + + operations = [ + migrations.RenameModel("TrackFile", "Upload"), + migrations.RenameField( + model_name="importjob", old_name="track_file", new_name="upload" + ), + migrations.RenameField( + model_name="library", old_name="files_count", new_name="uploads_count" + ), + migrations.AlterField( + model_name="upload", + name="library", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="uploads", + to="music.Library", + ), + ), + migrations.AlterField( + model_name="upload", + name="track", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="uploads", + to="music.Track", + ), + ), + ] diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 8e1577ea6..318640c88 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -1,13 +1,16 @@ +import datetime +import logging +import mimetypes import os -import shutil import tempfile import uuid import markdown import pendulum from django.conf import settings -from django.core.files import File +from django.contrib.postgres.fields import JSONField from django.core.files.base import ContentFile +from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver @@ -18,15 +21,28 @@ from taggit.managers import TaggableManager from versatileimagefield.fields import VersatileImageField from versatileimagefield.image_warmer import VersatileImageFieldWarmer -from funkwhale_api import downloader, musicbrainz +from funkwhale_api import musicbrainz +from funkwhale_api.common import fields +from funkwhale_api.common import session +from funkwhale_api.common import utils as common_utils +from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import utils as federation_utils - from . import importers, metadata, utils +logger = logging.getLogger(__file__) + + +def empty_dict(): + return {} + class APIModelMixin(models.Model): + fid = models.URLField(unique=True, max_length=500, db_index=True, null=True) mbid = models.UUIDField(unique=True, db_index=True, null=True, blank=True) uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4) + from_activity = models.ForeignKey( + "federation.Activity", null=True, blank=True, on_delete=models.SET_NULL + ) api_includes = [] creation_date = models.DateTimeField(default=timezone.now) import_hooks = [] @@ -79,6 +95,23 @@ class APIModelMixin(models.Model): self.musicbrainz_model, self.mbid ) + def get_federation_id(self): + if self.fid: + return self.fid + + return federation_utils.full_url( + reverse( + "federation:music:{}-detail".format(self.federation_namespace), + kwargs={"uuid": self.uuid}, + ) + ) + + def save(self, **kwargs): + if not self.pk and not self.fid: + self.fid = self.get_federation_id() + + return super().save(**kwargs) + class ArtistQuerySet(models.QuerySet): def with_albums_count(self): @@ -89,10 +122,27 @@ class ArtistQuerySet(models.QuerySet): models.Prefetch("albums", queryset=Album.objects.with_tracks_count()) ) + def annotate_playable_by_actor(self, actor): + tracks = ( + Track.objects.playable_by(actor) + .filter(artist=models.OuterRef("id")) + .order_by("id") + .values("id")[:1] + ) + subquery = models.Subquery(tracks) + return self.annotate(is_playable_by_actor=subquery) + + def playable_by(self, actor, include=True): + tracks = Track.objects.playable_by(actor, include) + if include: + return self.filter(tracks__in=tracks) + else: + return self.exclude(tracks__in=tracks) + class Artist(APIModelMixin): name = models.CharField(max_length=255) - + federation_namespace = "artists" musicbrainz_model = "artist" musicbrainz_mapping = { "mbid": {"musicbrainz_field_name": "id"}, @@ -140,6 +190,23 @@ class AlbumQuerySet(models.QuerySet): def with_tracks_count(self): return self.annotate(_tracks_count=models.Count("tracks")) + def annotate_playable_by_actor(self, actor): + tracks = ( + Track.objects.playable_by(actor) + .filter(album=models.OuterRef("id")) + .order_by("id") + .values("id")[:1] + ) + subquery = models.Subquery(tracks) + return self.annotate(is_playable_by_actor=subquery) + + def playable_by(self, actor, include=True): + tracks = Track.objects.playable_by(actor, include) + if include: + return self.filter(tracks__in=tracks) + else: + return self.exclude(tracks__in=tracks) + class Album(APIModelMixin): title = models.CharField(max_length=255) @@ -154,6 +221,7 @@ class Album(APIModelMixin): api_includes = ["artist-credits", "recordings", "media", "release-groups"] api = musicbrainz.api.releases + federation_namespace = "albums" musicbrainz_model = "release" musicbrainz_mapping = { "mbid": {"musicbrainz_field_name": "id"}, @@ -177,14 +245,35 @@ class Album(APIModelMixin): def get_image(self, data=None): if data: - f = ContentFile(data["content"]) extensions = {"image/jpeg": "jpg", "image/png": "png", "image/gif": "gif"} extension = extensions.get(data["mimetype"], "jpg") - self.cover.save("{}.{}".format(self.uuid, extension), f) - else: + if data.get("content"): + # we have to cover itself + f = ContentFile(data["content"]) + elif data.get("url"): + # we can fetch from a url + try: + response = session.get_session().get( + data.get("url"), + timeout=3, + verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, + ) + response.raise_for_status() + except Exception as e: + logger.warn( + "Cannot download cover at url %s: %s", data.get("url"), e + ) + return + else: + f = ContentFile(response.content) + self.cover.save("{}.{}".format(self.uuid, extension), f, save=False) + self.save(update_fields=["cover"]) + return self.cover.file + if self.mbid: image_data = musicbrainz.api.images.get_front(str(self.mbid)) f = ContentFile(image_data) - self.cover.save("{0}.jpg".format(self.mbid), f) + self.cover.save("{0}.jpg".format(self.mbid), f, save=False) + self.save(update_fields=["cover"]) return self.cover.file def __str__(self): @@ -249,6 +338,8 @@ class Work(APIModelMixin): api = musicbrainz.api.works api_includes = ["url-rels", "recording-rels"] musicbrainz_model = "work" + federation_namespace = "works" + musicbrainz_mapping = { "mbid": {"musicbrainz_field_name": "id"}, "title": {"musicbrainz_field_name": "title"}, @@ -266,6 +357,12 @@ class Work(APIModelMixin): return lyric + def get_federation_id(self): + if self.fid: + return self.fid + + return None + class Lyrics(models.Model): uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4) @@ -287,10 +384,37 @@ class Lyrics(models.Model): class TrackQuerySet(models.QuerySet): def for_nested_serialization(self): - return ( - self.select_related() - .select_related("album__artist", "artist") - .prefetch_related("files") + return self.select_related().select_related("album__artist", "artist") + + def annotate_playable_by_actor(self, actor): + files = ( + Upload.objects.playable_by(actor) + .filter(track=models.OuterRef("id")) + .order_by("id") + .values("id")[:1] + ) + subquery = models.Subquery(files) + return self.annotate(is_playable_by_actor=subquery) + + def playable_by(self, actor, include=True): + files = Upload.objects.playable_by(actor, include) + if include: + return self.filter(uploads__in=files) + else: + return self.exclude(uploads__in=files) + + def annotate_duration(self): + first_upload = Upload.objects.filter(track=models.OuterRef("pk")).order_by("pk") + return self.annotate( + duration=models.Subquery(first_upload.values("duration")[:1]) + ) + + def annotate_file_data(self): + first_upload = Upload.objects.filter(track=models.OuterRef("pk")).order_by("pk") + return self.annotate( + bitrate=models.Subquery(first_upload.values("bitrate")[:1]), + size=models.Subquery(first_upload.values("size")[:1]), + mimetype=models.Subquery(first_upload.values("mimetype")[:1]), ) @@ -310,7 +434,7 @@ class Track(APIModelMixin): work = models.ForeignKey( Work, related_name="tracks", null=True, blank=True, on_delete=models.CASCADE ) - + federation_namespace = "tracks" musicbrainz_model = "recording" api = musicbrainz.api.recordings api_includes = ["artist-credits", "releases", "media", "tags", "work-rels"] @@ -423,48 +547,139 @@ class Track(APIModelMixin): }, ) + @property + def listen_url(self): + return reverse("api:v1:listen-detail", kwargs={"uuid": self.uuid}) -class TrackFile(models.Model): + +class UploadQuerySet(models.QuerySet): + def playable_by(self, actor, include=True): + libraries = Library.objects.viewable_by(actor) + + if include: + return self.filter(library__in=libraries, import_status="finished") + return self.exclude(library__in=libraries, import_status="finished") + + def local(self, include=True): + return self.exclude(library__actor__user__isnull=include) + + def for_federation(self): + return self.filter(import_status="finished", mimetype__startswith="audio/") + + +TRACK_FILE_IMPORT_STATUS_CHOICES = ( + ("pending", "Pending"), + ("finished", "Finished"), + ("errored", "Errored"), + ("skipped", "Skipped"), +) + + +def get_file_path(instance, filename): + if instance.library.actor.get_user(): + return common_utils.ChunkedPath("tracks")(instance, filename) + else: + # we cache remote tracks in a different directory + return common_utils.ChunkedPath("federation_cache/tracks")(instance, filename) + + +def get_import_reference(): + return str(uuid.uuid4()) + + +class Upload(models.Model): + fid = models.URLField(unique=True, max_length=500, null=True, blank=True) uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4) - track = models.ForeignKey(Track, related_name="files", on_delete=models.CASCADE) - audio_file = models.FileField(upload_to="tracks/%Y/%m/%d", max_length=255) - source = models.URLField(null=True, blank=True, max_length=500) + track = models.ForeignKey( + Track, related_name="uploads", on_delete=models.CASCADE, null=True, blank=True + ) + audio_file = models.FileField(upload_to=get_file_path, max_length=255) + source = models.CharField( + # URL validators are not flexible enough for our file:// and upload:// schemes + null=True, + blank=True, + max_length=500, + ) creation_date = models.DateTimeField(default=timezone.now) - modification_date = models.DateTimeField(auto_now=True) + modification_date = models.DateTimeField(default=timezone.now, null=True) accessed_date = models.DateTimeField(null=True, blank=True) duration = models.IntegerField(null=True, blank=True) size = models.IntegerField(null=True, blank=True) bitrate = models.IntegerField(null=True, blank=True) acoustid_track_id = models.UUIDField(null=True, blank=True) mimetype = models.CharField(null=True, blank=True, max_length=200) - - library_track = models.OneToOneField( - "federation.LibraryTrack", - related_name="local_track_file", - on_delete=models.CASCADE, + library = models.ForeignKey( + "library", null=True, blank=True, + related_name="uploads", + on_delete=models.CASCADE, ) - def download_file(self): - # import the track file, since there is not any - # we create a tmp dir for the download - tmp_dir = tempfile.mkdtemp() - data = downloader.download(self.source, target_directory=tmp_dir) - self.duration = data.get("duration", None) - self.audio_file.save( - os.path.basename(data["audio_file_path"]), - File(open(data["audio_file_path"], "rb")), + # metadata from federation + metadata = JSONField( + default=empty_dict, max_length=50000, encoder=DjangoJSONEncoder + ) + import_date = models.DateTimeField(null=True, blank=True) + # optionnal metadata provided during import + import_metadata = JSONField( + default=empty_dict, max_length=50000, encoder=DjangoJSONEncoder + ) + # status / error details for the import + import_status = models.CharField( + default="pending", choices=TRACK_FILE_IMPORT_STATUS_CHOICES, max_length=25 + ) + # a short reference provided by the client to group multiple files + # in the same import + import_reference = models.CharField(max_length=50, default=get_import_reference) + + # optionnal metadata about import results (error messages, etc.) + import_details = JSONField( + default=empty_dict, max_length=50000, encoder=DjangoJSONEncoder + ) + from_activity = models.ForeignKey( + "federation.Activity", null=True, on_delete=models.SET_NULL + ) + + objects = UploadQuerySet.as_manager() + + def download_audio_from_remote(self, user): + from funkwhale_api.common import session + from funkwhale_api.federation import signing + + if user.is_authenticated and user.actor: + auth = signing.get_auth(user.actor.private_key, user.actor.private_key_id) + else: + auth = None + + remote_response = session.get_session().get( + self.source, + auth=auth, + stream=True, + timeout=20, + headers={"Content-Type": "application/octet-stream"}, + verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, ) - shutil.rmtree(tmp_dir) - return self.audio_file + with remote_response as r: + remote_response.raise_for_status() + extension = utils.get_ext_from_type(self.mimetype) + title = " - ".join( + [self.track.title, self.track.album.title, self.track.artist.name] + ) + filename = "{}.{}".format(title, extension) + tmp_file = tempfile.TemporaryFile() + for chunk in r.iter_content(chunk_size=512): + tmp_file.write(chunk) + self.audio_file.save(filename, tmp_file, save=False) + self.save(update_fields=["audio_file"]) - def get_federation_url(self): - return federation_utils.full_url("/federation/music/file/{}".format(self.uuid)) + def get_federation_id(self): + if self.fid: + return self.fid - @property - def path(self): - return reverse("api:v1:trackfiles-serve", kwargs={"pk": self.pk}) + return federation_utils.full_url( + reverse("federation:music:uploads-detail", kwargs={"uuid": self.uuid}) + ) @property def filename(self): @@ -483,37 +698,35 @@ class TrackFile(models.Model): if self.source.startswith("file://"): return os.path.getsize(self.source.replace("file://", "", 1)) - if self.library_track and self.library_track.audio_file: - return self.library_track.audio_file.size - def get_audio_file(self): if self.audio_file: return self.audio_file.open() if self.source.startswith("file://"): return open(self.source.replace("file://", "", 1), "rb") - if self.library_track and self.library_track.audio_file: - return self.library_track.audio_file.open() - def set_audio_data(self): + def get_audio_data(self): audio_file = self.get_audio_file() - if audio_file: - with audio_file as f: - audio_data = utils.get_audio_file_data(f) - if not audio_data: - return - self.duration = int(audio_data["length"]) - self.bitrate = audio_data["bitrate"] - self.size = self.get_file_size() - else: - lt = self.library_track - if lt: - self.duration = lt.get_metadata("length") - self.size = lt.get_metadata("size") - self.bitrate = lt.get_metadata("bitrate") + if not audio_file: + return + audio_data = utils.get_audio_file_data(audio_file) + if not audio_data: + return + return { + "duration": int(audio_data["length"]), + "bitrate": audio_data["bitrate"], + "size": self.get_file_size(), + } def save(self, **kwargs): - if not self.mimetype and self.audio_file: - self.mimetype = utils.guess_mimetype(self.audio_file) + if not self.mimetype: + if self.audio_file: + self.mimetype = utils.guess_mimetype(self.audio_file) + elif self.source and self.source.startswith("file://"): + self.mimetype = mimetypes.guess_type(self.source)[0] + if not self.size and self.audio_file: + self.size = self.audio_file.size + if not self.pk and not self.fid and self.library.actor.get_user(): + self.fid = self.get_federation_id() return super().save(**kwargs) def get_metadata(self): @@ -522,6 +735,10 @@ class TrackFile(models.Model): return return metadata.Metadata(audio_file) + @property + def listen_url(self): + return self.track.listen_url + "?upload={}".format(self.uuid) + IMPORT_STATUS_CHOICES = ( ("pending", "Pending"), @@ -559,6 +776,13 @@ class ImportBatch(models.Model): blank=True, on_delete=models.SET_NULL, ) + library = models.ForeignKey( + "Library", + related_name="import_batches", + null=True, + blank=True, + on_delete=models.CASCADE, + ) class Meta: ordering = ["-creation_date"] @@ -577,7 +801,7 @@ class ImportBatch(models.Model): tasks.import_batch_notify_followers.delay(import_batch_id=self.pk) - def get_federation_url(self): + def get_federation_id(self): return federation_utils.full_url( "/federation/music/import/batch/{}".format(self.uuid) ) @@ -589,8 +813,8 @@ class ImportJob(models.Model): batch = models.ForeignKey( ImportBatch, related_name="jobs", on_delete=models.CASCADE ) - track_file = models.ForeignKey( - TrackFile, related_name="jobs", null=True, blank=True, on_delete=models.CASCADE + upload = models.ForeignKey( + Upload, related_name="jobs", null=True, blank=True, on_delete=models.CASCADE ) source = models.CharField(max_length=500) mbid = models.UUIDField(editable=False, null=True, blank=True) @@ -609,10 +833,125 @@ class ImportJob(models.Model): null=True, blank=True, ) + audio_file_size = models.IntegerField(null=True, blank=True) class Meta: ordering = ("id",) + def save(self, **kwargs): + if self.audio_file and not self.audio_file_size: + self.audio_file_size = self.audio_file.size + return super().save(**kwargs) + + +LIBRARY_PRIVACY_LEVEL_CHOICES = [ + (k, l) for k, l in fields.PRIVACY_LEVEL_CHOICES if k != "followers" +] + + +class LibraryQuerySet(models.QuerySet): + def with_follows(self, actor): + return self.prefetch_related( + models.Prefetch( + "received_follows", + queryset=federation_models.LibraryFollow.objects.filter(actor=actor), + to_attr="_follows", + ) + ) + + def viewable_by(self, actor): + from funkwhale_api.federation.models import LibraryFollow + + if actor is None: + return Library.objects.filter(privacy_level="everyone") + + me_query = models.Q(privacy_level="me", actor=actor) + instance_query = models.Q(privacy_level="instance", actor__domain=actor.domain) + followed_libraries = LibraryFollow.objects.filter( + actor=actor, approved=True + ).values_list("target", flat=True) + return Library.objects.filter( + me_query + | instance_query + | models.Q(privacy_level="everyone") + | models.Q(pk__in=followed_libraries) + ) + + +class Library(federation_models.FederationMixin): + uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4) + actor = models.ForeignKey( + "federation.Actor", related_name="libraries", on_delete=models.CASCADE + ) + followers_url = models.URLField(max_length=500) + creation_date = models.DateTimeField(default=timezone.now) + name = models.CharField(max_length=100) + description = models.TextField(max_length=5000, null=True, blank=True) + privacy_level = models.CharField( + choices=LIBRARY_PRIVACY_LEVEL_CHOICES, default="me", max_length=25 + ) + uploads_count = models.PositiveIntegerField(default=0) + objects = LibraryQuerySet.as_manager() + + def get_federation_id(self): + return federation_utils.full_url( + reverse("federation:music:libraries-detail", kwargs={"uuid": self.uuid}) + ) + + def save(self, **kwargs): + if not self.pk and not self.fid and self.actor.get_user(): + self.fid = self.get_federation_id() + self.followers_url = self.fid + "/followers" + + return super().save(**kwargs) + + def should_autoapprove_follow(self, actor): + if self.privacy_level == "everyone": + return True + if self.privacy_level == "instance" and actor.get_user(): + return True + return False + + def schedule_scan(self, actor, force=False): + latest_scan = ( + self.scans.exclude(status="errored").order_by("-creation_date").first() + ) + delay_between_scans = datetime.timedelta(seconds=3600 * 24) + now = timezone.now() + if ( + not force + and latest_scan + and latest_scan.creation_date + delay_between_scans > now + ): + return + + scan = self.scans.create(total_files=self.uploads_count, actor=actor) + from . import tasks + + common_utils.on_commit(tasks.start_library_scan.delay, library_scan_id=scan.pk) + return scan + + +SCAN_STATUS = [ + ("pending", "pending"), + ("scanning", "scanning"), + ("errored", "errored"), + ("finished", "finished"), +] + + +class LibraryScan(models.Model): + actor = models.ForeignKey( + "federation.Actor", null=True, blank=True, on_delete=models.CASCADE + ) + library = models.ForeignKey(Library, related_name="scans", on_delete=models.CASCADE) + total_files = models.PositiveIntegerField(default=0) + processed_files = models.PositiveIntegerField(default=0) + errored_files = models.PositiveIntegerField(default=0) + status = models.CharField(default="pending", max_length=25) + creation_date = models.DateTimeField(default=timezone.now) + modification_date = models.DateTimeField(null=True, blank=True) + @receiver(post_save, sender=ImportJob) def update_batch_status(sender, instance, **kwargs): diff --git a/api/funkwhale_api/music/permissions.py b/api/funkwhale_api/music/permissions.py deleted file mode 100644 index dc589b5dd..000000000 --- a/api/funkwhale_api/music/permissions.py +++ /dev/null @@ -1,24 +0,0 @@ - -from rest_framework.permissions import BasePermission - -from funkwhale_api.common import preferences -from funkwhale_api.federation import actors, models - - -class Listen(BasePermission): - def has_permission(self, request, view): - if not preferences.get("common__api_authentication_required"): - return True - - user = getattr(request, "user", None) - if user and user.is_authenticated: - return True - - actor = getattr(request, "actor", None) - if actor is None: - return False - - library = actors.SYSTEM_ACTORS["library"].get_actor_instance() - return models.Follow.objects.filter( - target=library, actor=actor, approved=True - ).exists() diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 0661eb8f4..a23fc1daa 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -1,12 +1,14 @@ -from django.db.models import Q +from django.db import transaction from rest_framework import serializers from taggit.models import Tag from versatileimagefield.serializers import VersatileImageFieldSerializer from funkwhale_api.activity import serializers as activity_serializers -from funkwhale_api.users.serializers import UserBasicSerializer +from funkwhale_api.common import serializers as common_serializers +from funkwhale_api.common import utils as common_utils +from funkwhale_api.federation import routes -from . import models, tasks +from . import filters, models, tasks cover_field = VersatileImageFieldSerializer(allow_null=True, sizes="square") @@ -15,6 +17,7 @@ cover_field = VersatileImageFieldSerializer(allow_null=True, sizes="square") class ArtistAlbumSerializer(serializers.ModelSerializer): tracks_count = serializers.SerializerMethodField() cover = cover_field + is_playable = serializers.SerializerMethodField() class Meta: model = models.Album @@ -27,11 +30,18 @@ class ArtistAlbumSerializer(serializers.ModelSerializer): "cover", "creation_date", "tracks_count", + "is_playable", ) def get_tracks_count(self, o): return o._tracks_count + def get_is_playable(self, obj): + try: + return bool(obj.is_playable_by_actor) + except AttributeError: + return None + class ArtistWithAlbumsSerializer(serializers.ModelSerializer): albums = ArtistAlbumSerializer(many=True, read_only=True) @@ -41,30 +51,6 @@ class ArtistWithAlbumsSerializer(serializers.ModelSerializer): fields = ("id", "mbid", "name", "creation_date", "albums") -class TrackFileSerializer(serializers.ModelSerializer): - path = serializers.SerializerMethodField() - - class Meta: - model = models.TrackFile - fields = ( - "id", - "path", - "source", - "filename", - "mimetype", - "track", - "duration", - "mimetype", - "bitrate", - "size", - ) - read_only_fields = ["duration", "mimetype", "bitrate", "size"] - - def get_path(self, o): - url = o.path - return url - - class ArtistSimpleSerializer(serializers.ModelSerializer): class Meta: model = models.Artist @@ -72,8 +58,10 @@ class ArtistSimpleSerializer(serializers.ModelSerializer): class AlbumTrackSerializer(serializers.ModelSerializer): - files = TrackFileSerializer(many=True, read_only=True) artist = ArtistSimpleSerializer(read_only=True) + is_playable = serializers.SerializerMethodField() + listen_url = serializers.SerializerMethodField() + duration = serializers.SerializerMethodField() class Meta: model = models.Track @@ -84,15 +72,33 @@ class AlbumTrackSerializer(serializers.ModelSerializer): "album", "artist", "creation_date", - "files", "position", + "is_playable", + "listen_url", + "duration", ) + def get_is_playable(self, obj): + try: + return bool(obj.is_playable_by_actor) + except AttributeError: + return None + + def get_listen_url(self, obj): + return obj.listen_url + + def get_duration(self, obj): + try: + return obj.duration + except AttributeError: + return None + class AlbumSerializer(serializers.ModelSerializer): tracks = serializers.SerializerMethodField() artist = ArtistSimpleSerializer(read_only=True) cover = cover_field + is_playable = serializers.SerializerMethodField() class Meta: model = models.Album @@ -105,6 +111,7 @@ class AlbumSerializer(serializers.ModelSerializer): "release_date", "cover", "creation_date", + "is_playable", ) def get_tracks(self, o): @@ -114,6 +121,12 @@ class AlbumSerializer(serializers.ModelSerializer): ) return AlbumTrackSerializer(ordered_tracks, many=True).data + def get_is_playable(self, obj): + try: + return any([bool(t.is_playable_by_actor) for t in obj.tracks.all()]) + except AttributeError: + return None + class TrackAlbumSerializer(serializers.ModelSerializer): artist = ArtistSimpleSerializer(read_only=True) @@ -133,10 +146,15 @@ class TrackAlbumSerializer(serializers.ModelSerializer): class TrackSerializer(serializers.ModelSerializer): - files = TrackFileSerializer(many=True, read_only=True) artist = ArtistSimpleSerializer(read_only=True) album = TrackAlbumSerializer(read_only=True) lyrics = serializers.SerializerMethodField() + is_playable = serializers.SerializerMethodField() + listen_url = serializers.SerializerMethodField() + duration = serializers.SerializerMethodField() + bitrate = serializers.SerializerMethodField() + size = serializers.SerializerMethodField() + mimetype = serializers.SerializerMethodField() class Meta: model = models.Track @@ -147,14 +165,184 @@ class TrackSerializer(serializers.ModelSerializer): "album", "artist", "creation_date", - "files", "position", "lyrics", + "is_playable", + "listen_url", + "duration", + "bitrate", + "size", + "mimetype", ) def get_lyrics(self, obj): return obj.get_lyrics_url() + def get_listen_url(self, obj): + return obj.listen_url + + def get_is_playable(self, obj): + try: + return bool(obj.is_playable_by_actor) + except AttributeError: + return None + + def get_duration(self, obj): + try: + return obj.duration + except AttributeError: + return None + + def get_bitrate(self, obj): + try: + return obj.bitrate + except AttributeError: + return None + + def get_size(self, obj): + try: + return obj.size + except AttributeError: + return None + + def get_mimetype(self, obj): + try: + return obj.mimetype + except AttributeError: + return None + + +class LibraryForOwnerSerializer(serializers.ModelSerializer): + uploads_count = serializers.SerializerMethodField() + size = serializers.SerializerMethodField() + + class Meta: + model = models.Library + fields = [ + "uuid", + "fid", + "name", + "description", + "privacy_level", + "uploads_count", + "size", + "creation_date", + ] + read_only_fields = ["fid", "uuid", "creation_date", "actor"] + + def get_uploads_count(self, o): + return getattr(o, "_uploads_count", o.uploads_count) + + def get_size(self, o): + return getattr(o, "_size", 0) + + +class UploadSerializer(serializers.ModelSerializer): + track = TrackSerializer(required=False, allow_null=True) + library = common_serializers.RelatedField( + "uuid", + LibraryForOwnerSerializer(), + required=True, + filters=lambda context: {"actor": context["user"].actor}, + ) + + class Meta: + model = models.Upload + fields = [ + "uuid", + "filename", + "creation_date", + "mimetype", + "track", + "library", + "duration", + "mimetype", + "bitrate", + "size", + "import_date", + "import_status", + ] + + read_only_fields = [ + "uuid", + "creation_date", + "duration", + "mimetype", + "bitrate", + "size", + "track", + "import_date", + "import_status", + ] + + +class UploadForOwnerSerializer(UploadSerializer): + class Meta(UploadSerializer.Meta): + fields = UploadSerializer.Meta.fields + [ + "import_details", + "import_metadata", + "import_reference", + "metadata", + "source", + "audio_file", + ] + write_only_fields = ["audio_file"] + read_only_fields = UploadSerializer.Meta.read_only_fields + [ + "import_details", + "import_metadata", + "metadata", + ] + + def to_representation(self, obj): + r = super().to_representation(obj) + if "audio_file" in r: + del r["audio_file"] + return r + + def validate(self, validated_data): + if "audio_file" in validated_data: + self.validate_upload_quota(validated_data["audio_file"]) + + return super().validate(validated_data) + + def validate_upload_quota(self, f): + quota_status = self.context["user"].get_quota_status() + if (f.size / 1000 / 1000) > quota_status["remaining"]: + raise serializers.ValidationError("upload_quota_reached") + + return f + + +class UploadActionSerializer(common_serializers.ActionSerializer): + actions = [ + common_serializers.Action("delete", allow_all=True), + common_serializers.Action("relaunch_import", allow_all=True), + ] + filterset_class = filters.UploadFilter + pk_field = "uuid" + + @transaction.atomic + def handle_delete(self, objects): + libraries = sorted(set(objects.values_list("library", flat=True))) + for id in libraries: + # we group deletes by library for easier federation + uploads = objects.filter(library__pk=id).select_related("library__actor") + for chunk in common_utils.chunk_queryset(uploads, 100): + routes.outbox.dispatch( + {"type": "Delete", "object": {"type": "Audio"}}, + context={"uploads": chunk}, + ) + + return objects.delete() + + @transaction.atomic + def handle_relaunch_import(self, objects): + qs = objects.exclude(import_status="finished") + pks = list(qs.values_list("id", flat=True)) + qs.update(import_status="pending") + for pk in pks: + common_utils.on_commit(tasks.process_upload.delay, upload_id=pk) + class TagSerializer(serializers.ModelSerializer): class Meta: @@ -176,40 +364,6 @@ class LyricsSerializer(serializers.ModelSerializer): fields = ("id", "work", "content", "content_rendered") -class ImportJobSerializer(serializers.ModelSerializer): - track_file = TrackFileSerializer(read_only=True) - - class Meta: - model = models.ImportJob - fields = ("id", "mbid", "batch", "source", "status", "track_file", "audio_file") - read_only_fields = ("status", "track_file") - - -class ImportBatchSerializer(serializers.ModelSerializer): - submitted_by = UserBasicSerializer(read_only=True) - - class Meta: - model = models.ImportBatch - fields = ( - "id", - "submitted_by", - "source", - "status", - "creation_date", - "import_request", - ) - read_only_fields = ("creation_date", "submitted_by", "source") - - def to_representation(self, instance): - repr = super().to_representation(instance) - try: - repr["job_count"] = instance.job_count - except AttributeError: - # Queryset was not annotated - pass - return repr - - class TrackActivitySerializer(activity_serializers.ModelSerializer): type = serializers.SerializerMethodField() name = serializers.CharField(source="title") @@ -222,33 +376,3 @@ class TrackActivitySerializer(activity_serializers.ModelSerializer): def get_type(self, obj): return "Audio" - - -class ImportJobRunSerializer(serializers.Serializer): - jobs = serializers.PrimaryKeyRelatedField( - many=True, - queryset=models.ImportJob.objects.filter(status__in=["pending", "errored"]), - ) - batches = serializers.PrimaryKeyRelatedField( - many=True, queryset=models.ImportBatch.objects.all() - ) - - def validate(self, validated_data): - jobs = validated_data["jobs"] - batches_ids = [b.pk for b in validated_data["batches"]] - query = Q(batch__pk__in=batches_ids) - query |= Q(pk__in=[j.id for j in jobs]) - queryset = ( - models.ImportJob.objects.filter(query) - .filter(status__in=["pending", "errored"]) - .distinct() - ) - validated_data["_jobs"] = queryset - return validated_data - - def create(self, validated_data): - ids = validated_data["_jobs"].values_list("id", flat=True) - validated_data["_jobs"].update(status="pending") - for id in ids: - tasks.import_job_run.delay(import_job_id=id) - return {"jobs": list(ids)} diff --git a/api/funkwhale_api/music/signals.py b/api/funkwhale_api/music/signals.py new file mode 100644 index 000000000..47ea37e98 --- /dev/null +++ b/api/funkwhale_api/music/signals.py @@ -0,0 +1,5 @@ +import django.dispatch + +upload_import_status_updated = django.dispatch.Signal( + providing_args=["old_status", "new_status", "upload"] +) diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index 2092b6ee7..d96471b96 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -1,228 +1,54 @@ +import collections import logging import os -from django.conf import settings -from django.core.files.base import ContentFile -from musicbrainzngs import ResponseError +from django.utils import timezone +from django.db import transaction +from django.db.models import F, Q +from django.dispatch import receiver -from funkwhale_api.common import preferences -from funkwhale_api.federation import activity, actors -from funkwhale_api.federation import serializers as federation_serializers -from funkwhale_api.providers.acoustid import get_acoustid_client -from funkwhale_api.providers.audiofile import tasks as audiofile_tasks +from musicbrainzngs import ResponseError +from requests.exceptions import RequestException + +from funkwhale_api.common import channels +from funkwhale_api.federation import routes +from funkwhale_api.federation import library as lb from funkwhale_api.taskapp import celery from . import lyrics as lyrics_utils from . import models -from . import utils as music_utils +from . import metadata +from . import signals +from . import serializers logger = logging.getLogger(__name__) -@celery.app.task(name="acoustid.set_on_track_file") -@celery.require_instance(models.TrackFile, "track_file") -def set_acoustid_on_track_file(track_file): - client = get_acoustid_client() - result = client.get_best_match(track_file.audio_file.path) - - def update(id): - track_file.acoustid_track_id = id - track_file.save(update_fields=["acoustid_track_id"]) - return id - - if result: - return update(result["id"]) - - -def import_track_from_remote(library_track): - metadata = library_track.metadata - try: - track_mbid = metadata["recording"]["musicbrainz_id"] - assert track_mbid # for null/empty values - except (KeyError, AssertionError): - pass - else: - return models.Track.get_or_create_from_api(mbid=track_mbid)[0] - - try: - album_mbid = metadata["release"]["musicbrainz_id"] - assert album_mbid # for null/empty values - except (KeyError, AssertionError): - pass - else: - album, _ = models.Album.get_or_create_from_api(mbid=album_mbid) - return models.Track.get_or_create_from_title( - library_track.title, artist=album.artist, album=album - )[0] - - try: - artist_mbid = metadata["artist"]["musicbrainz_id"] - assert artist_mbid # for null/empty values - except (KeyError, AssertionError): - pass - else: - artist, _ = models.Artist.get_or_create_from_api(mbid=artist_mbid) - album, _ = models.Album.get_or_create_from_title( - library_track.album_title, artist=artist - ) - return models.Track.get_or_create_from_title( - library_track.title, artist=artist, album=album - )[0] - - # worst case scenario, we have absolutely no way to link to a - # musicbrainz resource, we rely on the name/titles - artist, _ = models.Artist.get_or_create_from_name(library_track.artist_name) - album, _ = models.Album.get_or_create_from_title( - library_track.album_title, artist=artist - ) - return models.Track.get_or_create_from_title( - library_track.title, artist=artist, album=album - )[0] - - -def _do_import(import_job, use_acoustid=False): - logger.info("[Import Job %s] starting job", import_job.pk) - from_file = bool(import_job.audio_file) - mbid = import_job.mbid - replace = import_job.replace_if_duplicate - acoustid_track_id = None - duration = None - track = None - # use_acoustid = use_acoustid and preferences.get('providers_acoustid__api_key') - # Acoustid is not reliable, we disable it for now. - use_acoustid = False - if not mbid and use_acoustid and from_file: - # we try to deduce mbid from acoustid - client = get_acoustid_client() - match = client.get_best_match(import_job.audio_file.path) - if match: - duration = match["recordings"][0]["duration"] - mbid = match["recordings"][0]["id"] - acoustid_track_id = match["id"] - if mbid: - logger.info( - "[Import Job %s] importing track from musicbrainz recording %s", - import_job.pk, - str(mbid), - ) - track, _ = models.Track.get_or_create_from_api(mbid=mbid) - elif import_job.audio_file: - logger.info( - "[Import Job %s] importing track from uploaded track data at %s", - import_job.pk, - import_job.audio_file.path, - ) - track = audiofile_tasks.import_track_data_from_path(import_job.audio_file.path) - elif import_job.library_track: - logger.info( - "[Import Job %s] importing track from federated library track %s", - import_job.pk, - import_job.library_track.pk, - ) - track = import_track_from_remote(import_job.library_track) - elif import_job.source.startswith("file://"): - tf_path = import_job.source.replace("file://", "", 1) - logger.info( - "[Import Job %s] importing track from local track data at %s", - import_job.pk, - tf_path, - ) - track = audiofile_tasks.import_track_data_from_path(tf_path) - else: - raise ValueError( - "Not enough data to process import, " - "add a mbid, an audio file or a library track" - ) - - track_file = None - if replace: - logger.info("[Import Job %s] deleting existing audio file", import_job.pk) - track.files.all().delete() - elif track.files.count() > 0: - logger.info( - "[Import Job %s] skipping, we already have a file for this track", - import_job.pk, - ) - if import_job.audio_file: - import_job.audio_file.delete() - import_job.status = "skipped" - import_job.save() - return - - track_file = track_file or models.TrackFile(track=track, source=import_job.source) - track_file.acoustid_track_id = acoustid_track_id - if from_file: - track_file.audio_file = ContentFile(import_job.audio_file.read()) - track_file.audio_file.name = import_job.audio_file.name - track_file.duration = duration - elif import_job.library_track: - track_file.library_track = import_job.library_track - track_file.mimetype = import_job.library_track.audio_mimetype - if import_job.library_track.library.download_files: - raise NotImplementedError() - else: - # no downloading, we hotlink - pass - elif not import_job.audio_file and not import_job.source.startswith("file://"): - # not an inplace import, and we have a source, so let's download it - logger.info("[Import Job %s] downloading audio file from remote", import_job.pk) - track_file.download_file() - elif not import_job.audio_file and import_job.source.startswith("file://"): - # in place import, we set mimetype from extension - path, ext = os.path.splitext(import_job.source) - track_file.mimetype = music_utils.get_type_from_ext(ext) - track_file.set_audio_data() - track_file.save() - # if no cover is set on track album, we try to update it as well: - if not track.album.cover: - logger.info("[Import Job %s] retrieving album cover", import_job.pk) - update_album_cover(track.album, track_file) - import_job.status = "finished" - import_job.track_file = track_file - if import_job.audio_file: - # it's imported on the track, we don't need it anymore - import_job.audio_file.delete() - import_job.save() - logger.info("[Import Job %s] job finished", import_job.pk) - return track_file - - -def update_album_cover(album, track_file, replace=False): +def update_album_cover(album, source=None, cover_data=None, replace=False): if album.cover and not replace: return + if cover_data: + return album.get_image(data=cover_data) - if track_file: - # maybe the file has a cover embedded? + if source and source.startswith("file://"): + # let's look for a cover in the same directory + path = os.path.dirname(source.replace("file://", "", 1)) + logger.info("[Album %s] scanning covers from %s", album.pk, path) + cover = get_cover_from_fs(path) + if cover: + return album.get_image(data=cover) + if album.mbid: try: - metadata = track_file.get_metadata() - except FileNotFoundError: - metadata = None - if metadata: - cover = metadata.get_picture("cover_front") - if cover: - # best case scenario, cover is embedded in the track - logger.info("[Album %s] Using cover embedded in file", album.pk) - return album.get_image(data=cover) - if track_file.source and track_file.source.startswith("file://"): - # let's look for a cover in the same directory - path = os.path.dirname(track_file.source.replace("file://", "", 1)) - logger.info("[Album %s] scanning covers from %s", album.pk, path) - cover = get_cover_from_fs(path) - if cover: - return album.get_image(data=cover) - if not album.mbid: - return - try: - logger.info( - "[Album %s] Fetching cover from musicbrainz release %s", - album.pk, - str(album.mbid), - ) - return album.get_image() - except ResponseError as exc: - logger.warning( - "[Album %s] cannot fetch cover from musicbrainz: %s", album.pk, str(exc) - ) + logger.info( + "[Album %s] Fetching cover from musicbrainz release %s", + album.pk, + str(album.mbid), + ) + return album.get_image() + except ResponseError as exc: + logger.warning( + "[Album %s] cannot fetch cover from musicbrainz: %s", album.pk, str(exc) + ) IMAGE_TYPES = [("jpg", "image/jpeg"), ("png", "image/png")] @@ -240,37 +66,6 @@ def get_cover_from_fs(dir_path): return {"mimetype": m, "content": c.read()} -@celery.app.task(name="ImportJob.run", bind=True) -@celery.require_instance( - models.ImportJob.objects.filter(status__in=["pending", "errored"]), "import_job" -) -def import_job_run(self, import_job, use_acoustid=False): - def mark_errored(exc): - logger.error("[Import Job %s] Error during import: %s", import_job.pk, str(exc)) - import_job.status = "errored" - import_job.save(update_fields=["status"]) - - try: - tf = _do_import(import_job, use_acoustid=use_acoustid) - return tf.pk if tf else None - except Exception as exc: - if not settings.DEBUG: - try: - self.retry(exc=exc, countdown=30, max_retries=3) - except Exception: - mark_errored(exc) - raise - mark_errored(exc) - raise - - -@celery.app.task(name="ImportBatch.run") -@celery.require_instance(models.ImportBatch, "import_batch") -def import_batch_run(import_batch): - for job_id in import_batch.jobs.order_by("id").values_list("id", flat=True): - import_job_run.delay(import_job_id=job_id) - - @celery.app.task(name="Lyrics.fetch_content") @celery.require_instance(models.Lyrics, "lyrics") def fetch_content(lyrics): @@ -281,40 +76,453 @@ def fetch_content(lyrics): lyrics.save(update_fields=["content"]) -@celery.app.task(name="music.import_batch_notify_followers") +@celery.app.task(name="music.start_library_scan") @celery.require_instance( - models.ImportBatch.objects.filter(status="finished"), "import_batch" + models.LibraryScan.objects.select_related().filter(status="pending"), "library_scan" ) -def import_batch_notify_followers(import_batch): - if not preferences.get("federation__enabled"): - return +def start_library_scan(library_scan): + try: + data = lb.get_library_data(library_scan.library.fid, actor=library_scan.actor) + except Exception: + library_scan.status = "errored" + library_scan.save(update_fields=["status", "modification_date"]) + raise + library_scan.modification_date = timezone.now() + library_scan.status = "scanning" + library_scan.total_files = data["totalItems"] + library_scan.save(update_fields=["status", "modification_date", "total_files"]) + scan_library_page.delay(library_scan_id=library_scan.pk, page_url=data["first"]) - if import_batch.source == "federation": - return - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - followers = library_actor.get_approved_followers() - jobs = import_batch.jobs.filter( - status="finished", library_track__isnull=True, track_file__isnull=False - ).select_related("track_file__track__artist", "track_file__track__album__artist") - track_files = [job.track_file for job in jobs] - collection = federation_serializers.CollectionSerializer( - { - "actor": library_actor, - "id": import_batch.get_federation_url(), - "items": track_files, - "item_serializer": federation_serializers.AudioSerializer, +@celery.app.task( + name="music.scan_library_page", + retry_backoff=60, + max_retries=5, + autoretry_for=[RequestException], +) +@celery.require_instance( + models.LibraryScan.objects.select_related().filter(status="scanning"), + "library_scan", +) +def scan_library_page(library_scan, page_url): + data = lb.get_library_page(library_scan.library, page_url, library_scan.actor) + uploads = [] + + for item_serializer in data["items"]: + upload = item_serializer.save(library=library_scan.library) + uploads.append(upload) + + library_scan.processed_files = F("processed_files") + len(uploads) + library_scan.modification_date = timezone.now() + update_fields = ["modification_date", "processed_files"] + + next_page = data.get("next") + fetch_next = next_page and next_page != page_url + + if not fetch_next: + update_fields.append("status") + library_scan.status = "finished" + library_scan.save(update_fields=update_fields) + + if fetch_next: + scan_library_page.delay(library_scan_id=library_scan.pk, page_url=next_page) + + +def getter(data, *keys, default=None): + if not data: + return default + v = data + for k in keys: + try: + v = v[k] + except KeyError: + return default + + return v + + +class UploadImportError(ValueError): + def __init__(self, code): + self.code = code + super().__init__(code) + + +def fail_import(upload, error_code): + old_status = upload.import_status + upload.import_status = "errored" + upload.import_details = {"error_code": error_code} + upload.import_date = timezone.now() + upload.save(update_fields=["import_details", "import_status", "import_date"]) + + broadcast = getter( + upload.import_metadata, "funkwhale", "config", "broadcast", default=True + ) + if broadcast: + signals.upload_import_status_updated.send( + old_status=old_status, + new_status=upload.import_status, + upload=upload, + sender=None, + ) + + +@celery.app.task(name="music.process_upload") +@celery.require_instance( + models.Upload.objects.filter(import_status="pending").select_related( + "library__actor__user" + ), + "upload", +) +def process_upload(upload): + import_metadata = upload.import_metadata or {} + old_status = upload.import_status + audio_file = upload.get_audio_file() + try: + additional_data = {} + if not audio_file: + # we can only rely on user proveded data + final_metadata = import_metadata + else: + # we use user provided data and data from the file itself + m = metadata.Metadata(audio_file) + file_metadata = m.all() + final_metadata = collections.ChainMap( + additional_data, import_metadata, file_metadata + ) + additional_data["cover_data"] = m.get_picture("cover_front") + additional_data["upload_source"] = upload.source + track = get_track_from_import_metadata(final_metadata) + except UploadImportError as e: + return fail_import(upload, e.code) + except Exception: + fail_import(upload, "unknown_error") + raise + + # under some situations, we want to skip the import ( + # for instance if the user already owns the files) + owned_duplicates = get_owned_duplicates(upload, track) + upload.track = track + + if owned_duplicates: + upload.import_status = "skipped" + upload.import_details = { + "code": "already_imported_in_owned_libraries", + "duplicates": list(owned_duplicates), } - ).data - for f in followers: - create = federation_serializers.ActivitySerializer( - { - "type": "Create", - "id": collection["id"], - "object": collection, - "actor": library_actor.url, - "to": [f.url], - } - ).data + upload.import_date = timezone.now() + upload.save( + update_fields=["import_details", "import_status", "import_date", "track"] + ) + signals.upload_import_status_updated.send( + old_status=old_status, + new_status=upload.import_status, + upload=upload, + sender=None, + ) + return - activity.deliver(create, on_behalf_of=library_actor, to=[f.url]) + # all is good, let's finalize the import + audio_data = upload.get_audio_data() + if audio_data: + upload.duration = audio_data["duration"] + upload.size = audio_data["size"] + upload.bitrate = audio_data["bitrate"] + upload.import_status = "finished" + upload.import_date = timezone.now() + upload.save( + update_fields=[ + "track", + "import_status", + "import_date", + "size", + "duration", + "bitrate", + ] + ) + broadcast = getter( + import_metadata, "funkwhale", "config", "broadcast", default=True + ) + if broadcast: + signals.upload_import_status_updated.send( + old_status=old_status, + new_status=upload.import_status, + upload=upload, + sender=None, + ) + dispatch_outbox = getter( + import_metadata, "funkwhale", "config", "dispatch_outbox", default=True + ) + if dispatch_outbox: + routes.outbox.dispatch( + {"type": "Create", "object": {"type": "Audio"}}, context={"upload": upload} + ) + + +def federation_audio_track_to_metadata(payload): + """ + Given a valid payload as returned by federation.serializers.TrackSerializer.validated_data, + returns a correct metadata payload for use with get_track_from_import_metadata. + """ + musicbrainz_recordingid = payload.get("musicbrainzId") + musicbrainz_artistid = payload["artists"][0].get("musicbrainzId") + musicbrainz_albumartistid = payload["album"]["artists"][0].get("musicbrainzId") + musicbrainz_albumid = payload["album"].get("musicbrainzId") + + new_data = { + "title": payload["name"], + "album": payload["album"]["name"], + "track_number": payload["position"], + "artist": payload["artists"][0]["name"], + "album_artist": payload["album"]["artists"][0]["name"], + "date": payload["album"].get("released"), + # musicbrainz + "musicbrainz_recordingid": str(musicbrainz_recordingid) + if musicbrainz_recordingid + else None, + "musicbrainz_artistid": str(musicbrainz_artistid) + if musicbrainz_artistid + else None, + "musicbrainz_albumartistid": str(musicbrainz_albumartistid) + if musicbrainz_albumartistid + else None, + "musicbrainz_albumid": str(musicbrainz_albumid) + if musicbrainz_albumid + else None, + # federation + "fid": payload["id"], + "artist_fid": payload["artists"][0]["id"], + "album_artist_fid": payload["album"]["artists"][0]["id"], + "album_fid": payload["album"]["id"], + "fdate": payload["published"], + "album_fdate": payload["album"]["published"], + "album_artist_fdate": payload["album"]["artists"][0]["published"], + "artist_fdate": payload["artists"][0]["published"], + } + cover = payload["album"].get("cover") + if cover: + new_data["cover_data"] = {"mimetype": cover["mediaType"], "url": cover["href"]} + return new_data + + +def get_owned_duplicates(upload, track): + """ + Ensure we skip duplicate tracks to avoid wasting user/instance storage + """ + owned_libraries = upload.library.actor.libraries.all() + return ( + models.Upload.objects.filter( + track__isnull=False, library__in=owned_libraries, track=track + ) + .exclude(pk=upload.pk) + .values_list("uuid", flat=True) + ) + + +def get_best_candidate_or_create(model, query, defaults, sort_fields): + """ + Like queryset.get_or_create() but does not crash if multiple objects + are returned on the get() call + """ + candidates = model.objects.filter(query) + if candidates: + + return sort_candidates(candidates, sort_fields)[0], False + + return model.objects.create(**defaults), True + + +def sort_candidates(candidates, important_fields): + """ + Given a list of objects and a list of fields, + will return a sorted list of those objects by score. + + Score is higher for objects that have a non-empty attribute + that is also present in important fields:: + + artist1 = Artist(mbid=None, fid=None) + artist2 = Artist(mbid="something", fid=None) + + # artist2 has a mbid, so is sorted first + assert sort_candidates([artist1, artist2], ['mbid'])[0] == artist2 + + Only supports string fields. + """ + + # map each fields to its score, giving a higher score to first fields + fields_scores = {f: i + 1 for i, f in enumerate(sorted(important_fields))} + candidates_with_scores = [] + for candidate in candidates: + current_score = 0 + for field, score in fields_scores.items(): + v = getattr(candidate, field, "") + if v: + current_score += score + + candidates_with_scores.append((candidate, current_score)) + + return [c for c, s in reversed(sorted(candidates_with_scores, key=lambda v: v[1]))] + + +@transaction.atomic +def get_track_from_import_metadata(data): + track_uuid = getter(data, "funkwhale", "track", "uuid") + + if track_uuid: + # easy case, we have a reference to a uuid of a track that + # already exists in our database + try: + track = models.Track.objects.get(uuid=track_uuid) + except models.Track.DoesNotExist: + raise UploadImportError(code="track_uuid_not_found") + + if not track.album.cover: + update_album_cover( + track.album, + source=data.get("upload_source"), + cover_data=data.get("cover_data"), + ) + return track + + from_activity_id = data.get("from_activity_id", None) + track_mbid = data.get("musicbrainz_recordingid", None) + album_mbid = data.get("musicbrainz_albumid", None) + track_fid = getter(data, "fid") + + query = None + + if album_mbid and track_mbid: + query = Q(mbid=track_mbid, album__mbid=album_mbid) + + if track_fid: + query = query | Q(fid=track_fid) if query else Q(fid=track_fid) + + if query: + # second easy case: we have a (track_mbid, album_mbid) pair or + # a federation uuid we can check on + try: + return sort_candidates(models.Track.objects.filter(query), ["mbid", "fid"])[ + 0 + ] + except IndexError: + pass + + # get / create artist and album artist + artist_mbid = data.get("musicbrainz_artistid", None) + artist_fid = data.get("artist_fid", None) + artist_name = data["artist"] + query = Q(name__iexact=artist_name) + if artist_mbid: + query |= Q(mbid=artist_mbid) + if artist_fid: + query |= Q(fid=artist_fid) + defaults = { + "name": artist_name, + "mbid": artist_mbid, + "fid": artist_fid, + "from_activity_id": from_activity_id, + } + if data.get("artist_fdate"): + defaults["creation_date"] = data.get("artist_fdate") + + artist = get_best_candidate_or_create( + models.Artist, query, defaults=defaults, sort_fields=["mbid", "fid"] + )[0] + + album_artist_name = data.get("album_artist") or artist_name + if album_artist_name == artist_name: + album_artist = artist + else: + query = Q(name__iexact=album_artist_name) + album_artist_mbid = data.get("musicbrainz_albumartistid", None) + album_artist_fid = data.get("album_artist_fid", None) + if album_artist_mbid: + query |= Q(mbid=album_artist_mbid) + if album_artist_fid: + query |= Q(fid=album_artist_fid) + defaults = { + "name": album_artist_name, + "mbid": album_artist_mbid, + "fid": album_artist_fid, + "from_activity_id": from_activity_id, + } + if data.get("album_artist_fdate"): + defaults["creation_date"] = data.get("album_artist_fdate") + + album_artist = get_best_candidate_or_create( + models.Artist, query, defaults=defaults, sort_fields=["mbid", "fid"] + )[0] + + # get / create album + album_title = data["album"] + album_fid = data.get("album_fid", None) + query = Q(title__iexact=album_title, artist=album_artist) + if album_mbid: + query |= Q(mbid=album_mbid) + if album_fid: + query |= Q(fid=album_fid) + defaults = { + "title": album_title, + "artist": album_artist, + "mbid": album_mbid, + "release_date": data.get("date"), + "fid": album_fid, + "from_activity_id": from_activity_id, + } + if data.get("album_fdate"): + defaults["creation_date"] = data.get("album_fdate") + + album = get_best_candidate_or_create( + models.Album, query, defaults=defaults, sort_fields=["mbid", "fid"] + )[0] + if not album.cover: + update_album_cover( + album, source=data.get("upload_source"), cover_data=data.get("cover_data") + ) + + # get / create track + track_title = data["title"] + track_number = data.get("track_number", 1) + query = Q(title__iexact=track_title, artist=artist, album=album) + if track_mbid: + query |= Q(mbid=track_mbid) + if track_fid: + query |= Q(fid=track_fid) + defaults = { + "title": track_title, + "album": album, + "mbid": track_mbid, + "artist": artist, + "position": track_number, + "fid": track_fid, + "from_activity_id": from_activity_id, + } + if data.get("fdate"): + defaults["creation_date"] = data.get("fdate") + + track = get_best_candidate_or_create( + models.Track, query, defaults=defaults, sort_fields=["mbid", "fid"] + )[0] + + return track + + +@receiver(signals.upload_import_status_updated) +def broadcast_import_status_update_to_owner(old_status, new_status, upload, **kwargs): + user = upload.library.actor.get_user() + if not user: + return + + group = "user.{}.imports".format(user.pk) + channels.group_send( + group, + { + "type": "event.send", + "text": "", + "data": { + "type": "import.status_updated", + "upload": serializers.UploadForOwnerSerializer(upload).data, + "old_status": old_status, + "new_status": new_status, + }, + }, + ) diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index 30f62f348..6da9ad949 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -54,7 +54,17 @@ def get_audio_file_data(f): if not data: return d = {} - d["bitrate"] = data.info.bitrate + d["bitrate"] = getattr(data.info, "bitrate", 0) d["length"] = data.info.length return d + + +def get_actor_from_request(request): + actor = None + if hasattr(request, "actor"): + actor = request.actor + elif request.user.is_authenticated: + actor = request.user.actor + + return actor diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 77a82dd21..871dfc920 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -1,36 +1,53 @@ -import json import logging import urllib from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist from django.db import transaction -from django.db.models import Count +from django.db.models import Count, Prefetch, Sum, F, Q from django.db.models.functions import Length from django.utils import timezone -from musicbrainzngs import ResponseError + from rest_framework import mixins +from rest_framework import permissions from rest_framework import settings as rest_settings from rest_framework import views, viewsets from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response from taggit.models import Tag -from funkwhale_api.common import utils as funkwhale_utils -from funkwhale_api.common.permissions import ConditionalAuthentication +from funkwhale_api.common import utils as common_utils +from funkwhale_api.common import permissions as common_permissions from funkwhale_api.federation.authentication import SignatureAuthentication -from funkwhale_api.federation.models import LibraryTrack -from funkwhale_api.musicbrainz import api -from funkwhale_api.requests.models import ImportRequest -from funkwhale_api.users.permissions import HasUserPermission +from funkwhale_api.federation import api_serializers as federation_api_serializers +from funkwhale_api.federation import routes -from . import filters, importers, models -from . import permissions as music_permissions -from . import serializers, tasks, utils +from . import filters, models, serializers, tasks, utils logger = logging.getLogger(__name__) +def get_libraries(filter_uploads): + def view(self, request, *args, **kwargs): + obj = self.get_object() + actor = utils.get_actor_from_request(request) + uploads = models.Upload.objects.all() + uploads = filter_uploads(obj, uploads) + uploads = uploads.playable_by(actor) + libraries = models.Library.objects.filter( + pk__in=uploads.values_list("library", flat=True) + ) + libraries = libraries.select_related("actor") + page = self.paginate_queryset(libraries) + if page is not None: + serializer = federation_api_serializers.LibrarySerializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = federation_api_serializers.LibrarySerializer(libraries, many=True) + return Response(serializer.data) + + return view + + class TagViewSetMixin(object): def get_queryset(self): queryset = super().get_queryset() @@ -41,107 +58,115 @@ class TagViewSetMixin(object): class ArtistViewSet(viewsets.ReadOnlyModelViewSet): - queryset = models.Artist.objects.with_albums() + queryset = models.Artist.objects.all() serializer_class = serializers.ArtistWithAlbumsSerializer - permission_classes = [ConditionalAuthentication] + permission_classes = [common_permissions.ConditionalAuthentication] filter_class = filters.ArtistFilter ordering_fields = ("id", "name", "creation_date") + def get_queryset(self): + queryset = super().get_queryset() + albums = models.Album.objects.with_tracks_count() + albums = albums.annotate_playable_by_actor( + utils.get_actor_from_request(self.request) + ) + return queryset.prefetch_related(Prefetch("albums", queryset=albums)).distinct() + + libraries = detail_route(methods=["get"])( + get_libraries( + filter_uploads=lambda o, uploads: uploads.filter( + Q(track__artist=o) | Q(track__album__artist=o) + ) + ) + ) + class AlbumViewSet(viewsets.ReadOnlyModelViewSet): queryset = ( - models.Album.objects.all() - .order_by("artist", "release_date") - .select_related() - .prefetch_related("tracks__artist", "tracks__files") + models.Album.objects.all().order_by("artist", "release_date").select_related() ) serializer_class = serializers.AlbumSerializer - permission_classes = [ConditionalAuthentication] + permission_classes = [common_permissions.ConditionalAuthentication] ordering_fields = ("creation_date", "release_date", "title") filter_class = filters.AlbumFilter + def get_queryset(self): + queryset = super().get_queryset() + tracks = models.Track.objects.annotate_playable_by_actor( + utils.get_actor_from_request(self.request) + ).select_related("artist") + if ( + hasattr(self, "kwargs") + and self.kwargs + and self.request.method.lower() == "get" + ): + # we are detailing a single album, so we can add the overhead + # to fetch additional data + tracks = tracks.annotate_duration() + qs = queryset.prefetch_related(Prefetch("tracks", queryset=tracks)) + return qs.distinct() -class ImportBatchViewSet( + libraries = detail_route(methods=["get"])( + get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track__album=o)) + ) + + +class LibraryViewSet( mixins.CreateModelMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, viewsets.GenericViewSet, ): + lookup_field = "uuid" queryset = ( - models.ImportBatch.objects.select_related() + models.Library.objects.all() .order_by("-creation_date") - .annotate(job_count=Count("jobs")) + .annotate(_uploads_count=Count("uploads")) + .annotate(_size=Sum("uploads__size")) ) - serializer_class = serializers.ImportBatchSerializer - permission_classes = (HasUserPermission,) - required_permissions = ["library", "upload"] - permission_operator = "or" - filter_class = filters.ImportBatchFilter - - def perform_create(self, serializer): - serializer.save(submitted_by=self.request.user) + serializer_class = serializers.LibraryForOwnerSerializer + permission_classes = [ + permissions.IsAuthenticated, + common_permissions.OwnerPermission, + ] + owner_field = "actor.user" + owner_checks = ["read", "write"] def get_queryset(self): qs = super().get_queryset() - # if user do not have library permission, we limit to their - # own jobs - if not self.request.user.has_permissions("library"): - qs = qs.filter(submitted_by=self.request.user) - return qs - - -class ImportJobViewSet( - mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet -): - queryset = models.ImportJob.objects.all().select_related() - serializer_class = serializers.ImportJobSerializer - permission_classes = (HasUserPermission,) - required_permissions = ["library", "upload"] - permission_operator = "or" - filter_class = filters.ImportJobFilter - - def get_queryset(self): - qs = super().get_queryset() - # if user do not have library permission, we limit to their - # own jobs - if not self.request.user.has_permissions("library"): - qs = qs.filter(batch__submitted_by=self.request.user) - return qs - - @list_route(methods=["get"]) - def stats(self, request, *args, **kwargs): - if not request.user.has_permissions("library"): - return Response(status=403) - qs = models.ImportJob.objects.all() - filterset = filters.ImportJobFilter(request.GET, queryset=qs) - qs = filterset.qs - qs = qs.values("status").order_by("status") - qs = qs.annotate(status_count=Count("status")) - - data = {} - for row in qs: - data[row["status"]] = row["status_count"] - - for s, _ in models.IMPORT_STATUS_CHOICES: - data.setdefault(s, 0) - - data["count"] = sum([v for v in data.values()]) - return Response(data) - - @list_route(methods=["post"]) - def run(self, request, *args, **kwargs): - serializer = serializers.ImportJobRunSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - payload = serializer.save() - - return Response(payload) + return qs.filter(actor=self.request.user.actor) def perform_create(self, serializer): - source = "file://" + serializer.validated_data["audio_file"].name - serializer.save(source=source) - funkwhale_utils.on_commit( - tasks.import_job_run.delay, import_job_id=serializer.instance.pk + serializer.save(actor=self.request.user.actor) + + @transaction.atomic + def perform_destroy(self, instance): + routes.outbox.dispatch( + {"type": "Delete", "object": {"type": "Library"}}, + context={"library": instance}, ) + instance.delete() + + @detail_route(methods=["get"]) + @transaction.non_atomic_requests + def follows(self, request, *args, **kwargs): + library = self.get_object() + queryset = ( + library.received_follows.filter(target__actor=self.request.user.actor) + .select_related("actor", "target__actor") + .order_by("-creation_date") + ) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = federation_api_serializers.LibraryFollowSerializer( + page, many=True + ) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet): @@ -151,14 +176,13 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet): queryset = models.Track.objects.all().for_nested_serialization() serializer_class = serializers.TrackSerializer - permission_classes = [ConditionalAuthentication] + permission_classes = [common_permissions.ConditionalAuthentication] filter_class = filters.TrackFilter ordering_fields = ( "creation_date", "title", - "album__title", "album__release_date", - "position", + "size", "artist__name", ) @@ -169,7 +193,18 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet): if user.is_authenticated and filter_favorites == "true": queryset = queryset.filter(track_favorites__user=user) - return queryset + queryset = queryset.annotate_playable_by_actor( + utils.get_actor_from_request(self.request) + ).annotate_duration() + if ( + hasattr(self, "kwargs") + and self.kwargs + and self.request.method.lower() == "get" + ): + # we are detailing a single track, so we can add the overhead + # to fetch additional data + queryset = queryset.annotate_file_data() + return queryset.distinct() @detail_route(methods=["get"]) @transaction.non_atomic_requests @@ -196,6 +231,10 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet): serializer = serializers.LyricsSerializer(lyrics) return Response(serializer.data) + libraries = detail_route(methods=["get"])( + get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track=o)) + ) + def get_file_path(audio_file): serve_path = settings.MUSIC_DIRECTORY_SERVE_PATH @@ -228,40 +267,37 @@ def get_file_path(audio_file): return path.encode("utf-8") -def handle_serve(track_file): - f = track_file +def handle_serve(upload, user): + f = upload # we update the accessed_date f.accessed_date = timezone.now() f.save(update_fields=["accessed_date"]) - mt = f.mimetype - audio_file = f.audio_file - try: - library_track = f.library_track - except ObjectDoesNotExist: - library_track = None - if library_track and not audio_file: - if not library_track.audio_file: - # we need to populate from cache - with transaction.atomic(): - # why the transaction/select_for_update? - # this is because browsers may send multiple requests - # in a short time range, for partial content, - # thus resulting in multiple downloads from the remote - qs = LibraryTrack.objects.select_for_update() - library_track = qs.get(pk=library_track.pk) - library_track.download_audio() - track_file.library_track = library_track - track_file.set_audio_data() - track_file.save(update_fields=["bitrate", "duration", "size"]) + if f.audio_file: + file_path = get_file_path(f.audio_file) - audio_file = library_track.audio_file - file_path = get_file_path(audio_file) - mt = library_track.audio_mimetype - elif audio_file: - file_path = get_file_path(audio_file) + elif f.source and ( + f.source.startswith("http://") or f.source.startswith("https://") + ): + # we need to populate from cache + with transaction.atomic(): + # why the transaction/select_for_update? + # this is because browsers may send multiple requests + # in a short time range, for partial content, + # thus resulting in multiple downloads from the remote + qs = f.__class__.objects.select_for_update() + f = qs.get(pk=f.pk) + f.download_audio_from_remote(user=user) + data = f.get_audio_data() + if data: + f.duration = data["duration"] + f.size = data["size"] + f.bitrate = data["bitrate"] + f.save(update_fields=["bitrate", "duration", "size"]) + file_path = get_file_path(f.audio_file) elif f.source and f.source.startswith("file://"): file_path = get_file_path(f.source.replace("file://", "", 1)) + mt = f.mimetype if mt: response = Response(content_type=mt) else: @@ -278,39 +314,100 @@ def handle_serve(track_file): return response -class TrackFileViewSet(viewsets.ReadOnlyModelViewSet): - queryset = ( - models.TrackFile.objects.all() - .select_related("track__artist", "track__album") - .order_by("-id") - ) - serializer_class = serializers.TrackFileSerializer +class ListenViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + queryset = models.Track.objects.all() + serializer_class = serializers.TrackSerializer authentication_classes = ( rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES + [SignatureAuthentication] ) - permission_classes = [music_permissions.Listen] + permission_classes = [common_permissions.ConditionalAuthentication] + lookup_field = "uuid" - @detail_route(methods=["get"]) - def serve(self, request, *args, **kwargs): - queryset = models.TrackFile.objects.select_related( - "library_track", "track__album__artist", "track__artist" - ) - try: - return handle_serve(queryset.get(pk=kwargs["pk"])) - except models.TrackFile.DoesNotExist: + def retrieve(self, request, *args, **kwargs): + track = self.get_object() + actor = utils.get_actor_from_request(request) + queryset = track.uploads.select_related("track__album__artist", "track__artist") + explicit_file = request.GET.get("upload") + if explicit_file: + queryset = queryset.filter(uuid=explicit_file) + queryset = queryset.playable_by(actor) + queryset = queryset.order_by(F("audio_file").desc(nulls_last=True)) + upload = queryset.first() + if not upload: return Response(status=404) + return handle_serve(upload, user=request.user) + + +class UploadViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): + lookup_field = "uuid" + queryset = ( + models.Upload.objects.all() + .order_by("-creation_date") + .select_related("library", "track__artist", "track__album__artist") + ) + serializer_class = serializers.UploadForOwnerSerializer + permission_classes = [ + permissions.IsAuthenticated, + common_permissions.OwnerPermission, + ] + owner_field = "library.actor.user" + owner_checks = ["read", "write"] + filter_class = filters.UploadFilter + ordering_fields = ( + "creation_date", + "import_date", + "bitrate", + "size", + "artist__name", + ) + + def get_queryset(self): + qs = super().get_queryset() + return qs.filter(library__actor=self.request.user.actor) + + @list_route(methods=["post"]) + def action(self, request, *args, **kwargs): + queryset = self.get_queryset() + serializer = serializers.UploadActionSerializer(request.data, queryset=queryset) + serializer.is_valid(raise_exception=True) + result = serializer.save() + return Response(result, status=200) + + def get_serializer_context(self): + context = super().get_serializer_context() + context["user"] = self.request.user + return context + + def perform_create(self, serializer): + upload = serializer.save() + common_utils.on_commit(tasks.process_upload.delay, upload_id=upload.pk) + + @transaction.atomic + def perform_destroy(self, instance): + routes.outbox.dispatch( + {"type": "Delete", "object": {"type": "Audio"}}, + context={"uploads": [instance]}, + ) + instance.delete() + class TagViewSet(viewsets.ReadOnlyModelViewSet): queryset = Tag.objects.all().order_by("name") serializer_class = serializers.TagSerializer - permission_classes = [ConditionalAuthentication] + permission_classes = [common_permissions.ConditionalAuthentication] class Search(views.APIView): max_results = 3 - permission_classes = [ConditionalAuthentication] + permission_classes = [common_permissions.ConditionalAuthentication] def get(self, request, *args, **kwargs): query = request.GET["query"] @@ -340,7 +437,6 @@ class Search(views.APIView): models.Track.objects.all() .filter(query_obj) .select_related("artist", "album__artist") - .prefetch_related("files") )[: self.max_results] def get_albums(self, query): @@ -350,7 +446,7 @@ class Search(views.APIView): models.Album.objects.all() .filter(query_obj) .select_related() - .prefetch_related("tracks__files") + .prefetch_related("tracks") )[: self.max_results] def get_artists(self, query): @@ -372,99 +468,3 @@ class Search(views.APIView): ) return qs.filter(query_obj)[: self.max_results] - - -class SubmitViewSet(viewsets.ViewSet): - queryset = models.ImportBatch.objects.none() - permission_classes = (HasUserPermission,) - required_permissions = ["library"] - - @list_route(methods=["post"]) - @transaction.non_atomic_requests - def single(self, request, *args, **kwargs): - try: - models.Track.objects.get(mbid=request.POST["mbid"]) - return Response({}) - except models.Track.DoesNotExist: - pass - batch = models.ImportBatch.objects.create(submitted_by=request.user) - job = models.ImportJob.objects.create( - mbid=request.POST["mbid"], batch=batch, source=request.POST["import_url"] - ) - tasks.import_job_run.delay(import_job_id=job.pk) - serializer = serializers.ImportBatchSerializer(batch) - return Response(serializer.data, status=201) - - def get_import_request(self, data): - try: - raw = data["importRequest"] - except KeyError: - return - - pk = int(raw) - try: - return ImportRequest.objects.get(pk=pk) - except ImportRequest.DoesNotExist: - pass - - @list_route(methods=["post"]) - @transaction.non_atomic_requests - def album(self, request, *args, **kwargs): - data = json.loads(request.body.decode("utf-8")) - import_request = self.get_import_request(data) - import_data, batch = self._import_album( - data, request, batch=None, import_request=import_request - ) - return Response(import_data) - - @transaction.atomic - def _import_album(self, data, request, batch=None, import_request=None): - # we import the whole album here to prevent race conditions that occurs - # when using get_or_create_from_api in tasks - album_data = api.releases.get( - id=data["releaseId"], includes=models.Album.api_includes - )["release"] - cleaned_data = models.Album.clean_musicbrainz_data(album_data) - album = importers.load( - models.Album, cleaned_data, album_data, import_hooks=[models.import_tracks] - ) - try: - album.get_image() - except ResponseError: - pass - if not batch: - batch = models.ImportBatch.objects.create( - submitted_by=request.user, import_request=import_request - ) - for row in data["tracks"]: - try: - models.TrackFile.objects.get(track__mbid=row["mbid"]) - except models.TrackFile.DoesNotExist: - job = models.ImportJob.objects.create( - mbid=row["mbid"], batch=batch, source=row["source"] - ) - funkwhale_utils.on_commit( - tasks.import_job_run.delay, import_job_id=job.pk - ) - - serializer = serializers.ImportBatchSerializer(batch) - return serializer.data, batch - - @list_route(methods=["post"]) - @transaction.non_atomic_requests - def artist(self, request, *args, **kwargs): - data = json.loads(request.body.decode("utf-8")) - import_request = self.get_import_request(data) - artist_data = api.artists.get(id=data["artistId"])["artist"] - cleaned_data = models.Artist.clean_musicbrainz_data(artist_data) - importers.load(models.Artist, cleaned_data, artist_data, import_hooks=[]) - - import_data = [] - batch = None - for row in data["albums"]: - row_data, batch = self._import_album( - row, request, batch=batch, import_request=import_request - ) - import_data.append(row_data) - - return Response(import_data[0]) diff --git a/api/funkwhale_api/musicbrainz/client.py b/api/funkwhale_api/musicbrainz/client.py index deae0672f..1355da943 100644 --- a/api/funkwhale_api/musicbrainz/client.py +++ b/api/funkwhale_api/musicbrainz/client.py @@ -6,6 +6,7 @@ from funkwhale_api import __version__ _api = musicbrainzngs _api.set_useragent("funkwhale", str(__version__), settings.FUNKWHALE_URL) +_api.set_hostname(settings.MUSICBRAINZ_HOSTNAME) store = memoize.djangocache.Cache("default") diff --git a/api/funkwhale_api/playlists/admin.py b/api/funkwhale_api/playlists/admin.py index 98ced232e..056535270 100644 --- a/api/funkwhale_api/playlists/admin.py +++ b/api/funkwhale_api/playlists/admin.py @@ -1,4 +1,4 @@ -from django.contrib import admin +from funkwhale_api.common import admin from . import models diff --git a/api/funkwhale_api/playlists/filters.py b/api/funkwhale_api/playlists/filters.py index 144b0f049..1f12521f0 100644 --- a/api/funkwhale_api/playlists/filters.py +++ b/api/funkwhale_api/playlists/filters.py @@ -8,7 +8,7 @@ from . import models class PlaylistFilter(filters.FilterSet): q = filters.CharFilter(name="_", method="filter_q") - listenable = filters.BooleanFilter(name="_", method="filter_listenable") + playable = filters.BooleanFilter(name="_", method="filter_playable") class Meta: model = models.Playlist @@ -16,10 +16,10 @@ class PlaylistFilter(filters.FilterSet): "user": ["exact"], "name": ["exact", "icontains"], "q": "exact", - "listenable": "exact", + "playable": "exact", } - def filter_listenable(self, queryset, name, value): + def filter_playable(self, queryset, name, value): queryset = queryset.annotate(plts_count=Count("playlist_tracks")) if value: return queryset.filter(plts_count__gt=0) diff --git a/api/funkwhale_api/playlists/models.py b/api/funkwhale_api/playlists/models.py index d2504d848..ac86b97a0 100644 --- a/api/funkwhale_api/playlists/models.py +++ b/api/funkwhale_api/playlists/models.py @@ -12,7 +12,7 @@ class PlaylistQuerySet(models.QuerySet): def with_duration(self): return self.annotate( - duration=models.Sum("playlist_tracks__track__files__duration") + duration=models.Sum("playlist_tracks__track__uploads__duration") ) def with_covers(self): @@ -38,6 +38,23 @@ class PlaylistQuerySet(models.QuerySet): ) return self.prefetch_related(plt_prefetch) + def annotate_playable_by_actor(self, actor): + plts = ( + PlaylistTrack.objects.playable_by(actor) + .filter(playlist=models.OuterRef("id")) + .order_by("id") + .values("id")[:1] + ) + subquery = models.Subquery(plts) + return self.annotate(is_playable_by_actor=subquery) + + def playable_by(self, actor, include=True): + plts = PlaylistTrack.objects.playable_by(actor, include) + if include: + return self.filter(playlist_tracks__in=plts) + else: + return self.exclude(playlist_tracks__in=plts) + class Playlist(models.Model): name = models.CharField(max_length=50) @@ -130,15 +147,30 @@ class Playlist(models.Model): class PlaylistTrackQuerySet(models.QuerySet): - def for_nested_serialization(self): - return ( - self.select_related() - .select_related("track__album__artist") - .prefetch_related( - "track__tags", "track__files", "track__artist__albums__tracks__tags" - ) + def for_nested_serialization(self, actor=None): + tracks = music_models.Track.objects.annotate_playable_by_actor(actor) + tracks = tracks.select_related("artist", "album__artist") + return self.prefetch_related( + models.Prefetch("track", queryset=tracks, to_attr="_prefetched_track") ) + def annotate_playable_by_actor(self, actor): + tracks = ( + music_models.Track.objects.playable_by(actor) + .filter(pk=models.OuterRef("track")) + .order_by("id") + .values("id")[:1] + ) + subquery = models.Subquery(tracks) + return self.annotate(is_playable_by_actor=subquery) + + def playable_by(self, actor, include=True): + tracks = music_models.Track.objects.playable_by(actor, include) + if include: + return self.filter(track__pk__in=tracks) + else: + return self.exclude(track__pk__in=tracks) + class PlaylistTrack(models.Model): track = models.ForeignKey( diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py index a60a34938..c1ca84e15 100644 --- a/api/funkwhale_api/playlists/serializers.py +++ b/api/funkwhale_api/playlists/serializers.py @@ -10,12 +10,17 @@ from . import models class PlaylistTrackSerializer(serializers.ModelSerializer): - track = TrackSerializer() + # track = TrackSerializer() + track = serializers.SerializerMethodField() class Meta: model = models.PlaylistTrack fields = ("id", "track", "playlist", "index", "creation_date") + def get_track(self, o): + track = o._prefetched_track if hasattr(o, "_prefetched_track") else o.track + return TrackSerializer(track).data + class PlaylistTrackWriteSerializer(serializers.ModelSerializer): index = serializers.IntegerField(required=False, min_value=0, allow_null=True) @@ -68,6 +73,7 @@ class PlaylistSerializer(serializers.ModelSerializer): duration = serializers.SerializerMethodField(read_only=True) album_covers = serializers.SerializerMethodField(read_only=True) user = UserBasicSerializer(read_only=True) + is_playable = serializers.SerializerMethodField() class Meta: model = models.Playlist @@ -81,9 +87,16 @@ class PlaylistSerializer(serializers.ModelSerializer): "tracks_count", "album_covers", "duration", + "is_playable", ) read_only_fields = ["id", "modification_date", "creation_date"] + def get_is_playable(self, obj): + try: + return bool(obj.is_playable_by_actor) + except AttributeError: + return None + def get_tracks_count(self, obj): try: return obj.tracks_count diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py index 8db076a86..4934b92a0 100644 --- a/api/funkwhale_api/playlists/views.py +++ b/api/funkwhale_api/playlists/views.py @@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response from funkwhale_api.common import fields, permissions - +from funkwhale_api.music import utils as music_utils from . import filters, models, serializers @@ -39,7 +39,9 @@ class PlaylistViewSet( @detail_route(methods=["get"]) def tracks(self, request, *args, **kwargs): playlist = self.get_object() - plts = playlist.playlist_tracks.all().for_nested_serialization() + plts = playlist.playlist_tracks.all().for_nested_serialization( + music_utils.get_actor_from_request(request) + ) serializer = serializers.PlaylistTrackSerializer(plts, many=True) data = {"count": len(plts), "results": serializer.data} return Response(data, status=200) @@ -59,7 +61,7 @@ class PlaylistViewSet( plts = ( models.PlaylistTrack.objects.filter(pk__in=ids) .order_by("index") - .for_nested_serialization() + .for_nested_serialization(music_utils.get_actor_from_request(request)) ) serializer = serializers.PlaylistTrackSerializer(plts, many=True) data = {"count": len(plts), "results": serializer.data} @@ -74,7 +76,9 @@ class PlaylistViewSet( return Response(status=204) def get_queryset(self): - return self.queryset.filter(fields.privacy_level_query(self.request.user)) + return self.queryset.filter( + fields.privacy_level_query(self.request.user) + ).annotate_playable_by_actor(music_utils.get_actor_from_request(self.request)) def perform_create(self, serializer): return serializer.save( @@ -95,7 +99,7 @@ class PlaylistTrackViewSet( ): serializer_class = serializers.PlaylistTrackSerializer - queryset = models.PlaylistTrack.objects.all().for_nested_serialization() + queryset = models.PlaylistTrack.objects.all() permission_classes = [ permissions.ConditionalAuthentication, permissions.OwnerPermission, @@ -116,7 +120,7 @@ class PlaylistTrackViewSet( lookup_field="playlist__privacy_level", user_field="playlist__user", ) - ) + ).for_nested_serialization(music_utils.get_actor_from_request(self.request)) def perform_destroy(self, instance): instance.delete(update_indexes=True) diff --git a/api/funkwhale_api/providers/audiofile/__init__.py b/api/funkwhale_api/providers/audiofile/__init__.py deleted file mode 100644 index 18e2c469a..000000000 --- a/api/funkwhale_api/providers/audiofile/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -This module is responsible from importing existing audiofiles from the -filesystem into funkwhale. -""" diff --git a/api/funkwhale_api/providers/audiofile/management/__init__.py b/api/funkwhale_api/providers/audiofile/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/funkwhale_api/providers/audiofile/management/commands/__init__.py b/api/funkwhale_api/providers/audiofile/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/funkwhale_api/providers/audiofile/tasks.py b/api/funkwhale_api/providers/audiofile/tasks.py deleted file mode 100644 index ee486345a..000000000 --- a/api/funkwhale_api/providers/audiofile/tasks.py +++ /dev/null @@ -1,45 +0,0 @@ -from django.db import transaction - -from funkwhale_api.music import metadata, models - - -@transaction.atomic -def import_track_data_from_path(path): - data = metadata.Metadata(path) - album = None - track_mbid = data.get("musicbrainz_recordingid", None) - album_mbid = data.get("musicbrainz_albumid", None) - - if album_mbid and track_mbid: - # to gain performance and avoid additional mb lookups, - # we import from the release data, which is already cached - return models.Track.get_or_create_from_release(album_mbid, track_mbid)[0] - elif track_mbid: - return models.Track.get_or_create_from_api(track_mbid)[0] - elif album_mbid: - album = models.Album.get_or_create_from_api(album_mbid)[0] - - artist = album.artist if album else None - artist_mbid = data.get("musicbrainz_artistid", None) - if not artist: - if artist_mbid: - artist = models.Artist.get_or_create_from_api(artist_mbid)[0] - else: - artist = models.Artist.objects.get_or_create( - name__iexact=data.get("artist"), defaults={"name": data.get("artist")} - )[0] - - release_date = data.get("date", default=None) - if not album: - album = models.Album.objects.get_or_create( - title__iexact=data.get("album"), - artist=artist, - defaults={"title": data.get("album"), "release_date": release_date}, - )[0] - position = data.get("track_number", default=None) - track = models.Track.objects.get_or_create( - title__iexact=data.get("title"), - album=album, - defaults={"title": data.get("title"), "position": position}, - )[0] - return track diff --git a/api/funkwhale_api/providers/urls.py b/api/funkwhale_api/providers/urls.py index 55a1193f5..dc8afeee7 100644 --- a/api/funkwhale_api/providers/urls.py +++ b/api/funkwhale_api/providers/urls.py @@ -1,16 +1,10 @@ from django.conf.urls import include, url urlpatterns = [ - url( - r"^youtube/", - include( - ("funkwhale_api.providers.youtube.urls", "youtube"), namespace="youtube" - ), - ), url( r"^musicbrainz/", include( ("funkwhale_api.musicbrainz.urls", "musicbrainz"), namespace="musicbrainz" ), - ), + ) ] diff --git a/api/funkwhale_api/providers/youtube/__init__.py b/api/funkwhale_api/providers/youtube/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/funkwhale_api/providers/youtube/client.py b/api/funkwhale_api/providers/youtube/client.py deleted file mode 100644 index 2235fcdc8..000000000 --- a/api/funkwhale_api/providers/youtube/client.py +++ /dev/null @@ -1,80 +0,0 @@ -import threading - -from apiclient.discovery import build -from dynamic_preferences.registries import global_preferences_registry as registry - -YOUTUBE_API_SERVICE_NAME = "youtube" -YOUTUBE_API_VERSION = "v3" -VIDEO_BASE_URL = "https://www.youtube.com/watch?v={0}" - - -def _do_search(query): - manager = registry.manager() - youtube = build( - YOUTUBE_API_SERVICE_NAME, - YOUTUBE_API_VERSION, - developerKey=manager["providers_youtube__api_key"], - ) - - return youtube.search().list(q=query, part="id,snippet", maxResults=25).execute() - - -class Client(object): - def search(self, query): - search_response = _do_search(query) - videos = [] - for search_result in search_response.get("items", []): - if search_result["id"]["kind"] == "youtube#video": - search_result["full_url"] = VIDEO_BASE_URL.format( - search_result["id"]["videoId"] - ) - videos.append(search_result) - return videos - - def search_multiple(self, queries): - results = {} - - def search(key, query): - results[key] = self.search(query) - - threads = [ - threading.Thread(target=search, args=(key, query)) - for key, query in queries.items() - ] - for thread in threads: - thread.start() - for thread in threads: - thread.join() - - return results - - def to_funkwhale(self, result): - """ - We convert youtube results to something more generic. - - { - "id": "video id", - "type": "youtube#video", - "url": "https://www.youtube.com/watch?v=id", - "description": "description", - "channelId": "Channel id", - "title": "Title", - "channelTitle": "channel Title", - "publishedAt": "2012-08-22T18:41:03.000Z", - "cover": "http://coverurl" - } - """ - return { - "id": result["id"]["videoId"], - "url": "https://www.youtube.com/watch?v={}".format(result["id"]["videoId"]), - "type": result["id"]["kind"], - "title": result["snippet"]["title"], - "description": result["snippet"]["description"], - "channelId": result["snippet"]["channelId"], - "channelTitle": result["snippet"]["channelTitle"], - "publishedAt": result["snippet"]["publishedAt"], - "cover": result["snippet"]["thumbnails"]["high"]["url"], - } - - -client = Client() diff --git a/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py b/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py deleted file mode 100644 index 2d950eb6b..000000000 --- a/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py +++ /dev/null @@ -1,16 +0,0 @@ -from django import forms -from dynamic_preferences.registries import global_preferences_registry -from dynamic_preferences.types import Section, StringPreference - -youtube = Section("providers_youtube") - - -@global_preferences_registry.register -class APIKey(StringPreference): - section = youtube - name = "api_key" - default = "CHANGEME" - verbose_name = "YouTube API key" - help_text = "The API key used to query YouTube. Get one at https://console.developers.google.com/." - widget = forms.PasswordInput - field_kwargs = {"required": False} diff --git a/api/funkwhale_api/providers/youtube/urls.py b/api/funkwhale_api/providers/youtube/urls.py deleted file mode 100644 index d9687ac9f..000000000 --- a/api/funkwhale_api/providers/youtube/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.conf.urls import url - -from .views import APISearch, APISearchs - -urlpatterns = [ - url(r"^search/$", APISearch.as_view(), name="search"), - url(r"^searchs/$", APISearchs.as_view(), name="searchs"), -] diff --git a/api/funkwhale_api/providers/youtube/views.py b/api/funkwhale_api/providers/youtube/views.py deleted file mode 100644 index 5e1982f48..000000000 --- a/api/funkwhale_api/providers/youtube/views.py +++ /dev/null @@ -1,27 +0,0 @@ -from rest_framework.response import Response -from rest_framework.views import APIView - -from funkwhale_api.common.permissions import ConditionalAuthentication - -from .client import client - - -class APISearch(APIView): - permission_classes = [ConditionalAuthentication] - - def get(self, request, *args, **kwargs): - results = client.search(request.GET["query"]) - return Response([client.to_funkwhale(result) for result in results]) - - -class APISearchs(APIView): - permission_classes = [ConditionalAuthentication] - - def post(self, request, *args, **kwargs): - results = client.search_multiple(request.data) - return Response( - { - key: [client.to_funkwhale(result) for result in group] - for key, group in results.items() - } - ) diff --git a/api/funkwhale_api/radios/admin.py b/api/funkwhale_api/radios/admin.py index 187950aeb..0cfd5d925 100644 --- a/api/funkwhale_api/radios/admin.py +++ b/api/funkwhale_api/radios/admin.py @@ -1,4 +1,4 @@ -from django.contrib import admin +from funkwhale_api.common import admin from . import models diff --git a/api/funkwhale_api/radios/radios.py b/api/funkwhale_api/radios/radios.py index c7c361de9..8d9eb816a 100644 --- a/api/funkwhale_api/radios/radios.py +++ b/api/funkwhale_api/radios/radios.py @@ -43,8 +43,8 @@ class SessionRadio(SimpleRadio): return self.session def get_queryset(self, **kwargs): - qs = Track.objects.annotate(files_count=Count("files")) - return qs.filter(files_count__gt=0) + qs = Track.objects.annotate(uploads_count=Count("uploads")) + return qs.filter(uploads_count__gt=0) def get_queryset_kwargs(self): return {} @@ -54,6 +54,8 @@ class SessionRadio(SimpleRadio): queryset = self.get_queryset(**kwargs) if self.session: queryset = self.filter_from_session(queryset) + if kwargs.pop("filter_playable", True): + queryset = queryset.playable_by(self.session.user.actor) return queryset def filter_from_session(self, queryset): diff --git a/api/funkwhale_api/requests/admin.py b/api/funkwhale_api/requests/admin.py deleted file mode 100644 index b0f1a7990..000000000 --- a/api/funkwhale_api/requests/admin.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.contrib import admin - -from . import models - - -@admin.register(models.ImportRequest) -class ImportRequestAdmin(admin.ModelAdmin): - list_display = ["artist_name", "user", "status", "creation_date"] - list_select_related = ["user"] - list_filter = ["status"] - search_fields = ["artist_name", "comment", "albums"] diff --git a/api/funkwhale_api/requests/api_urls.py b/api/funkwhale_api/requests/api_urls.py deleted file mode 100644 index 403a0953b..000000000 --- a/api/funkwhale_api/requests/api_urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework import routers - -from . import views - -router = routers.SimpleRouter() -router.register(r"import-requests", views.ImportRequestViewSet, "import-requests") - -urlpatterns = router.urls diff --git a/api/funkwhale_api/requests/factories.py b/api/funkwhale_api/requests/factories.py deleted file mode 100644 index d6673aebd..000000000 --- a/api/funkwhale_api/requests/factories.py +++ /dev/null @@ -1,15 +0,0 @@ -import factory - -from funkwhale_api.factories import registry -from funkwhale_api.users.factories import UserFactory - - -@registry.register -class ImportRequestFactory(factory.django.DjangoModelFactory): - artist_name = factory.Faker("name") - albums = factory.Faker("sentence") - user = factory.SubFactory(UserFactory) - comment = factory.Faker("paragraph") - - class Meta: - model = "requests.ImportRequest" diff --git a/api/funkwhale_api/requests/filters.py b/api/funkwhale_api/requests/filters.py deleted file mode 100644 index 4a06dea1b..000000000 --- a/api/funkwhale_api/requests/filters.py +++ /dev/null @@ -1,20 +0,0 @@ -import django_filters - -from funkwhale_api.common import fields - -from . import models - - -class ImportRequestFilter(django_filters.FilterSet): - - q = fields.SearchFilter( - search_fields=["artist_name", "user__username", "albums", "comment"] - ) - - class Meta: - model = models.ImportRequest - fields = { - "artist_name": ["exact", "iexact", "startswith", "icontains"], - "status": ["exact"], - "user__username": ["exact"], - } diff --git a/api/funkwhale_api/requests/serializers.py b/api/funkwhale_api/requests/serializers.py deleted file mode 100644 index 2a810a999..000000000 --- a/api/funkwhale_api/requests/serializers.py +++ /dev/null @@ -1,27 +0,0 @@ -from rest_framework import serializers - -from funkwhale_api.users.serializers import UserBasicSerializer - -from . import models - - -class ImportRequestSerializer(serializers.ModelSerializer): - user = UserBasicSerializer(read_only=True) - - class Meta: - model = models.ImportRequest - fields = ( - "id", - "status", - "albums", - "artist_name", - "user", - "creation_date", - "imported_date", - "comment", - ) - read_only_fields = ("creation_date", "imported_date", "user", "status") - - def create(self, validated_data): - validated_data["user"] = self.context["user"] - return super().create(validated_data) diff --git a/api/funkwhale_api/requests/views.py b/api/funkwhale_api/requests/views.py deleted file mode 100644 index 96d8c8927..000000000 --- a/api/funkwhale_api/requests/views.py +++ /dev/null @@ -1,27 +0,0 @@ -from rest_framework import mixins, viewsets - -from . import filters, models, serializers - - -class ImportRequestViewSet( - mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet, -): - - serializer_class = serializers.ImportRequestSerializer - queryset = ( - models.ImportRequest.objects.all().select_related().order_by("-creation_date") - ) - filter_class = filters.ImportRequestFilter - ordering_fields = ("id", "artist_name", "creation_date", "status") - - def perform_create(self, serializer): - return serializer.save(user=self.request.user) - - def get_serializer_context(self): - context = super().get_serializer_context() - if self.request.user.is_authenticated: - context["user"] = self.request.user - return context diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py index fc21a99f2..35b178641 100644 --- a/api/funkwhale_api/subsonic/serializers.py +++ b/api/funkwhale_api/subsonic/serializers.py @@ -24,7 +24,8 @@ class GetArtistsSerializer(serializers.Serializer): first_letter_mapping = collections.defaultdict(list) for artist in values: - first_letter_mapping[artist["name"][0].upper()].append(artist) + if artist["name"]: + first_letter_mapping[artist["name"][0].upper()].append(artist) for letter, artists in sorted(first_letter_mapping.items()): letter_data = { @@ -37,7 +38,7 @@ class GetArtistsSerializer(serializers.Serializer): class GetArtistSerializer(serializers.Serializer): def to_representation(self, artist): - albums = artist.albums.prefetch_related("tracks__files") + albums = artist.albums.prefetch_related("tracks__uploads") payload = { "id": artist.pk, "name": artist.name, @@ -61,7 +62,7 @@ class GetArtistSerializer(serializers.Serializer): return payload -def get_track_data(album, track, tf): +def get_track_data(album, track, upload): data = { "id": track.pk, "isDir": "false", @@ -69,9 +70,9 @@ def get_track_data(album, track, tf): "album": album.title, "artist": album.artist.name, "track": track.position or 1, - "contentType": tf.mimetype, - "suffix": tf.extension or "", - "duration": tf.duration or 0, + "contentType": upload.mimetype, + "suffix": upload.extension or "", + "duration": upload.duration or 0, "created": track.creation_date, "albumId": album.pk, "artistId": album.artist.pk, @@ -79,10 +80,10 @@ def get_track_data(album, track, tf): } if track.album.cover: data["coverArt"] = "al-{}".format(track.album.id) - if tf.bitrate: - data["bitrate"] = int(tf.bitrate / 1000) - if tf.size: - data["size"] = tf.size + if upload.bitrate: + data["bitrate"] = int(upload.bitrate / 1000) + if upload.size: + data["size"] = upload.size if album.release_date: data["year"] = album.release_date.year return data @@ -102,7 +103,7 @@ def get_album2_data(album): try: payload["songCount"] = album._tracks_count except AttributeError: - payload["songCount"] = len(album.tracks.prefetch_related("files")) + payload["songCount"] = len(album.tracks.prefetch_related("uploads")) return payload @@ -110,17 +111,17 @@ def get_song_list_data(tracks): songs = [] for track in tracks: try: - tf = [tf for tf in track.files.all()][0] + uploads = [upload for upload in track.uploads.all()][0] except IndexError: continue - track_data = get_track_data(track.album, track, tf) + track_data = get_track_data(track.album, track, uploads) songs.append(track_data) return songs class GetAlbumSerializer(serializers.Serializer): def to_representation(self, album): - tracks = album.tracks.prefetch_related("files").select_related("album") + tracks = album.tracks.prefetch_related("uploads").select_related("album") payload = get_album2_data(album) if album.release_date: payload["year"] = album.release_date.year @@ -129,21 +130,29 @@ class GetAlbumSerializer(serializers.Serializer): return payload +class GetSongSerializer(serializers.Serializer): + def to_representation(self, track): + uploads = track.uploads.all() + if not len(uploads): + return {} + return get_track_data(track.album, track, uploads[0]) + + def get_starred_tracks_data(favorites): by_track_id = {f.track_id: f for f in favorites} tracks = ( music_models.Track.objects.filter(pk__in=by_track_id.keys()) .select_related("album__artist") - .prefetch_related("files") + .prefetch_related("uploads") ) tracks = tracks.order_by("-creation_date") data = [] for t in tracks: try: - tf = [tf for tf in t.files.all()][0] + uploads = [upload for upload in t.uploads.all()][0] except IndexError: continue - td = get_track_data(t.album, t, tf) + td = get_track_data(t.album, t, uploads) td["starred"] = by_track_id[t.pk].creation_date data.append(td) return data @@ -169,26 +178,26 @@ def get_playlist_detail_data(playlist): data = get_playlist_data(playlist) qs = ( playlist.playlist_tracks.select_related("track__album__artist") - .prefetch_related("track__files") + .prefetch_related("track__uploads") .order_by("index") ) data["entry"] = [] for plt in qs: try: - tf = [tf for tf in plt.track.files.all()][0] + uploads = [upload for upload in plt.track.uploads.all()][0] except IndexError: continue - td = get_track_data(plt.track.album, plt.track, tf) + td = get_track_data(plt.track.album, plt.track, uploads) data["entry"].append(td) return data def get_music_directory_data(artist): - tracks = artist.tracks.select_related("album").prefetch_related("files") + tracks = artist.tracks.select_related("album").prefetch_related("uploads") data = {"id": artist.pk, "parent": 1, "name": artist.name, "child": []} for track in tracks: try: - tf = [tf for tf in track.files.all()][0] + upload = [upload for upload in track.uploads.all()][0] except IndexError: continue album = track.album @@ -200,19 +209,19 @@ def get_music_directory_data(artist): "artist": artist.name, "track": track.position or 1, "year": track.album.release_date.year if track.album.release_date else 0, - "contentType": tf.mimetype, - "suffix": tf.extension or "", - "duration": tf.duration or 0, + "contentType": upload.mimetype, + "suffix": upload.extension or "", + "duration": upload.duration or 0, "created": track.creation_date, "albumId": album.pk, "artistId": artist.pk, "parent": artist.id, "type": "music", } - if tf.bitrate: - td["bitrate"] = int(tf.bitrate / 1000) - if tf.size: - td["size"] = tf.size + if upload.bitrate: + td["bitrate"] = int(upload.bitrate / 1000) + if upload.size: + td["size"] = upload.size data["child"].append(td) return data @@ -220,9 +229,9 @@ def get_music_directory_data(artist): class ScrobbleSerializer(serializers.Serializer): submission = serializers.BooleanField(default=True, required=False) id = serializers.PrimaryKeyRelatedField( - queryset=music_models.Track.objects.annotate(files_count=Count("files")).filter( - files_count__gt=0 - ) + queryset=music_models.Track.objects.annotate( + uploads_count=Count("uploads") + ).filter(uploads_count__gt=0) ) def create(self, data): diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index bb5f44166..7ca9b13a8 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -19,7 +19,9 @@ from funkwhale_api.playlists import models as playlists_models from . import authentication, filters, negotiation, serializers -def find_object(queryset, model_field="pk", field="id", cast=int): +def find_object( + queryset, model_field="pk", field="id", cast=int, filter_playable=False +): def decorator(func): def inner(self, request, *args, **kwargs): data = request.GET or request.POST @@ -38,7 +40,7 @@ def find_object(queryset, model_field="pk", field="id", cast=int): ) try: value = cast(raw_value) - except (TypeError, ValidationError): + except (ValueError, TypeError, ValidationError): return response.Response( { "error": { @@ -50,6 +52,11 @@ def find_object(queryset, model_field="pk", field="id", cast=int): qs = queryset if hasattr(qs, "__call__"): qs = qs(request) + + if filter_playable: + actor = utils.get_actor_from_request(request) + qs = qs.playable_by(actor).distinct() + try: obj = qs.get(**{model_field: value}) except qs.model.DoesNotExist: @@ -124,7 +131,9 @@ class SubsonicViewSet(viewsets.GenericViewSet): @list_route(methods=["get", "post"], url_name="get_artists", url_path="getArtists") def get_artists(self, request, *args, **kwargs): - artists = music_models.Artist.objects.all() + artists = music_models.Artist.objects.all().playable_by( + utils.get_actor_from_request(request) + ) data = serializers.GetArtistsSerializer(artists).data payload = {"artists": data} @@ -132,14 +141,16 @@ class SubsonicViewSet(viewsets.GenericViewSet): @list_route(methods=["get", "post"], url_name="get_indexes", url_path="getIndexes") def get_indexes(self, request, *args, **kwargs): - artists = music_models.Artist.objects.all() + artists = music_models.Artist.objects.all().playable_by( + utils.get_actor_from_request(request) + ) data = serializers.GetArtistsSerializer(artists).data payload = {"indexes": data} return response.Response(payload, status=200) @list_route(methods=["get", "post"], url_name="get_artist", url_path="getArtist") - @find_object(music_models.Artist.objects.all()) + @find_object(music_models.Artist.objects.all(), filter_playable=True) def get_artist(self, request, *args, **kwargs): artist = kwargs.pop("obj") data = serializers.GetArtistSerializer(artist).data @@ -147,17 +158,28 @@ class SubsonicViewSet(viewsets.GenericViewSet): return response.Response(payload, status=200) + @list_route(methods=["get", "post"], url_name="get_song", url_path="getSong") + @find_object(music_models.Track.objects.all(), filter_playable=True) + def get_song(self, request, *args, **kwargs): + track = kwargs.pop("obj") + data = serializers.GetSongSerializer(track).data + payload = {"song": data} + + return response.Response(payload, status=200) + @list_route( methods=["get", "post"], url_name="get_artist_info2", url_path="getArtistInfo2" ) - @find_object(music_models.Artist.objects.all()) + @find_object(music_models.Artist.objects.all(), filter_playable=True) def get_artist_info2(self, request, *args, **kwargs): payload = {"artist-info2": {}} return response.Response(payload, status=200) @list_route(methods=["get", "post"], url_name="get_album", url_path="getAlbum") - @find_object(music_models.Album.objects.select_related("artist")) + @find_object( + music_models.Album.objects.select_related("artist"), filter_playable=True + ) def get_album(self, request, *args, **kwargs): album = kwargs.pop("obj") data = serializers.GetAlbumSerializer(album).data @@ -165,16 +187,14 @@ class SubsonicViewSet(viewsets.GenericViewSet): return response.Response(payload, status=200) @list_route(methods=["get", "post"], url_name="stream", url_path="stream") - @find_object(music_models.Track.objects.all()) + @find_object(music_models.Track.objects.all(), filter_playable=True) def stream(self, request, *args, **kwargs): track = kwargs.pop("obj") - queryset = track.files.select_related( - "library_track", "track__album__artist", "track__artist" - ) - track_file = queryset.first() - if not track_file: + queryset = track.uploads.select_related("track__album__artist", "track__artist") + upload = queryset.first() + if not upload: return response.Response(status=404) - return music_views.handle_serve(track_file) + return music_views.handle_serve(upload=upload, user=request.user) @list_route(methods=["get", "post"], url_name="star", url_path="star") @find_object(music_models.Track.objects.all()) @@ -214,6 +234,9 @@ class SubsonicViewSet(viewsets.GenericViewSet): data = request.GET or request.POST filterset = filters.AlbumList2FilterSet(data, queryset=queryset) queryset = filterset.qs + actor = utils.get_actor_from_request(request) + queryset = queryset.playable_by(actor) + try: offset = int(data["offset"]) except (TypeError, KeyError, ValueError): @@ -233,6 +256,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): def search3(self, request, *args, **kwargs): data = request.GET or request.POST query = str(data.get("query", "")).replace("*", "") + actor = utils.get_actor_from_request(request) conf = [ { "subsonic": "artist", @@ -258,9 +282,9 @@ class SubsonicViewSet(viewsets.GenericViewSet): "subsonic": "song", "search_fields": ["title"], "queryset": ( - music_models.Track.objects.prefetch_related("files").select_related( - "album__artist" - ) + music_models.Track.objects.prefetch_related( + "uploads" + ).select_related("album__artist") ), "serializer": serializers.get_song_list_data, }, @@ -285,6 +309,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): queryset = c["queryset"].filter( utils.get_query(query, c["search_fields"]) ) + queryset = queryset.playable_by(actor) queryset = queryset[offset : offset + size] payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset) return response.Response(payload) diff --git a/api/funkwhale_api/taskapp/celery.py b/api/funkwhale_api/taskapp/celery.py index 98e980f07..92fb2a0d9 100644 --- a/api/funkwhale_api/taskapp/celery.py +++ b/api/funkwhale_api/taskapp/celery.py @@ -1,21 +1,29 @@ - from __future__ import absolute_import import functools +import traceback as tb import os - -from celery import Celery +import logging +import celery.app.task from django.apps import AppConfig from django.conf import settings + +logger = logging.getLogger("celery") + if not settings.configured: # set the default Django settings module for the 'celery' program. os.environ.setdefault( "DJANGO_SETTINGS_MODULE", "config.settings.local" ) # pragma: no cover +app = celery.Celery("funkwhale_api") -app = Celery("funkwhale_api") + +@celery.signals.task_failure.connect +def process_failure(sender, task_id, exception, args, kwargs, traceback, einfo, **kw): + print("[celery] Error during task {}: {}".format(task_id, einfo.exception)) + tb.print_exc() class CeleryConfig(AppConfig): diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py index 365db615e..f7ee90c7e 100644 --- a/api/funkwhale_api/users/admin.py +++ b/api/funkwhale_api/users/admin.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from django import forms -from django.contrib import admin +from funkwhale_api.common import admin from django.contrib.auth.admin import UserAdmin as AuthUserAdmin from django.contrib.auth.forms import UserChangeForm, UserCreationForm from django.utils.translation import ugettext_lazy as _ diff --git a/api/funkwhale_api/users/dynamic_preferences_registry.py b/api/funkwhale_api/users/dynamic_preferences_registry.py index 08f5730a8..4e910577f 100644 --- a/api/funkwhale_api/users/dynamic_preferences_registry.py +++ b/api/funkwhale_api/users/dynamic_preferences_registry.py @@ -28,3 +28,13 @@ class DefaultPermissions(common_preferences.StringListPreference): help_text = "A list of default preferences to give to all registered users." choices = [(k, c["label"]) for k, c in models.PERMISSIONS_CONFIGURATION.items()] field_kwargs = {"choices": choices, "required": False} + + +@global_preferences_registry.register +class UploadQuota(types.IntPreference): + show_in_api = True + section = users + name = "upload_quota" + default = 1000 + verbose_name = "Upload quota" + help_text = "Default upload quota applied to each users, in MB. This can be overrided on a per-user basis." diff --git a/api/funkwhale_api/users/migrations/0008_auto_20180617_1531.py b/api/funkwhale_api/users/migrations/0008_auto_20180617_1531.py index b731e3279..b000024d0 100644 --- a/api/funkwhale_api/users/migrations/0008_auto_20180617_1531.py +++ b/api/funkwhale_api/users/migrations/0008_auto_20180617_1531.py @@ -5,19 +5,21 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0007_auto_20180524_2009'), - ] + dependencies = [("users", "0007_auto_20180524_2009")] operations = [ migrations.AddField( - model_name='user', - name='last_activity', + model_name="user", + name="last_activity", field=models.DateTimeField(blank=True, default=None, null=True), ), migrations.AlterField( - model_name='user', - name='permission_library', - field=models.BooleanField(default=False, help_text='Manage library, delete files, tracks, artists, albums...', verbose_name='Manage library'), + model_name="user", + name="permission_library", + field=models.BooleanField( + default=False, + help_text="Manage library, delete files, tracks, artists, albums...", + verbose_name="Manage library", + ), ), ] diff --git a/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py b/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py index e8204c4e4..cb9f12c60 100644 --- a/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py +++ b/api/funkwhale_api/users/migrations/0009_auto_20180619_2024.py @@ -8,24 +8,46 @@ import django.utils.timezone class Migration(migrations.Migration): - dependencies = [ - ('users', '0008_auto_20180617_1531'), - ] + dependencies = [("users", "0008_auto_20180617_1531")] operations = [ migrations.CreateModel( - name='Invitation', + name="Invitation", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('expiration_date', models.DateTimeField()), - ('code', models.CharField(max_length=50, unique=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("expiration_date", models.DateTimeField()), + ("code", models.CharField(max_length=50, unique=True)), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invitations", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AddField( - model_name='user', - name='invitation', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='users.Invitation'), + model_name="user", + name="invitation", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="users", + to="users.Invitation", + ), ), ] diff --git a/api/funkwhale_api/users/migrations/0010_user_avatar.py b/api/funkwhale_api/users/migrations/0010_user_avatar.py index da60439be..e50aa4061 100644 --- a/api/funkwhale_api/users/migrations/0010_user_avatar.py +++ b/api/funkwhale_api/users/migrations/0010_user_avatar.py @@ -7,14 +7,22 @@ import funkwhale_api.common.validators class Migration(migrations.Migration): - dependencies = [ - ('users', '0009_auto_20180619_2024'), - ] + dependencies = [("users", "0009_auto_20180619_2024")] operations = [ migrations.AddField( - model_name='user', - name='avatar', - field=models.ImageField(blank=True, max_length=150, null=True, upload_to=funkwhale_api.common.utils.ChunkedPath('users/avatars'), validators=[funkwhale_api.common.validators.ImageDimensionsValidator(max_height=400, max_width=400, min_height=50, min_width=50)]), - ), + model_name="user", + name="avatar", + field=models.ImageField( + blank=True, + max_length=150, + null=True, + upload_to=funkwhale_api.common.utils.ChunkedPath("users/avatars"), + validators=[ + funkwhale_api.common.validators.ImageDimensionsValidator( + max_height=400, max_width=400, min_height=50, min_width=50 + ) + ], + ), + ) ] diff --git a/api/funkwhale_api/users/migrations/0011_auto_20180721_1317.py b/api/funkwhale_api/users/migrations/0011_auto_20180721_1317.py index 5b5a1cabc..5115eed86 100644 --- a/api/funkwhale_api/users/migrations/0011_auto_20180721_1317.py +++ b/api/funkwhale_api/users/migrations/0011_auto_20180721_1317.py @@ -10,19 +10,41 @@ import versatileimagefield.fields class Migration(migrations.Migration): dependencies = [ - ('federation', '0006_auto_20180521_1702'), - ('users', '0010_user_avatar'), + ("federation", "0006_auto_20180521_1702"), + ("users", "0010_user_avatar"), ] operations = [ migrations.AddField( - model_name='user', - name='actor', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='federation.Actor'), + model_name="user", + name="actor", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="user", + to="federation.Actor", + ), ), migrations.AlterField( - model_name='user', - name='avatar', - field=versatileimagefield.fields.VersatileImageField(blank=True, max_length=150, null=True, upload_to=funkwhale_api.common.utils.ChunkedPath('users/avatars', preserve_file_name=False), validators=[funkwhale_api.common.validators.ImageDimensionsValidator(min_height=50, min_width=50), funkwhale_api.common.validators.FileValidator(allowed_extensions=['png', 'jpg', 'jpeg', 'gif'], max_size=2097152)]), + model_name="user", + name="avatar", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + max_length=150, + null=True, + upload_to=funkwhale_api.common.utils.ChunkedPath( + "users/avatars", preserve_file_name=False + ), + validators=[ + funkwhale_api.common.validators.ImageDimensionsValidator( + min_height=50, min_width=50 + ), + funkwhale_api.common.validators.FileValidator( + allowed_extensions=["png", "jpg", "jpeg", "gif"], + max_size=2097152, + ), + ], + ), ), ] diff --git a/api/funkwhale_api/users/migrations/0012_user_upload_quota.py b/api/funkwhale_api/users/migrations/0012_user_upload_quota.py new file mode 100644 index 000000000..64e47343c --- /dev/null +++ b/api/funkwhale_api/users/migrations/0012_user_upload_quota.py @@ -0,0 +1,16 @@ +# Generated by Django 2.0.7 on 2018-08-01 16:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("users", "0011_auto_20180721_1317")] + + operations = [ + migrations.AddField( + model_name="user", + name="upload_quota", + field=models.PositiveIntegerField(blank=True, null=True), + ) + ] diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index 26ffb5a94..2bc87588e 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -17,6 +17,7 @@ from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +from django_auth_ldap.backend import populate_user as ldap_populate_user from versatileimagefield.fields import VersatileImageField from versatileimagefield.image_warmer import VersatileImageFieldWarmer @@ -121,6 +122,8 @@ class User(AbstractUser): blank=True, ) + upload_quota = models.PositiveIntegerField(null=True, blank=True) + def __str__(self): return self.username @@ -181,6 +184,32 @@ class User(AbstractUser): self.last_activity = now self.save(update_fields=["last_activity"]) + def create_actor(self): + self.actor = create_actor(self) + self.save(update_fields=["actor"]) + return self.actor + + def get_upload_quota(self): + return self.upload_quota or preferences.get("users__upload_quota") + + def get_quota_status(self): + data = self.actor.get_current_usage() + max_ = self.get_upload_quota() + return { + "max": max_, + "remaining": max(max_ - (data["total"] / 1000 / 1000), 0), + "current": data["total"] / 1000 / 1000, + "skipped": data["skipped"] / 1000 / 1000, + "pending": data["pending"] / 1000 / 1000, + "finished": data["finished"] / 1000 / 1000, + "errored": data["errored"] / 1000 / 1000, + } + + def get_channels_groups(self): + groups = ["imports", "inbox"] + + return ["user.{}.{}".format(self.pk, g) for g in groups] + def generate_code(length=10): return "".join( @@ -219,32 +248,50 @@ class Invitation(models.Model): return super().save(**kwargs) -def create_actor(user): - username = user.username - private, public = keys.get_key_pair() - args = { +def get_actor_data(user): + username = federation_utils.slugify_username(user.username) + return { "preferred_username": username, "domain": settings.FEDERATION_HOSTNAME, "type": "Person", - "name": username, + "name": user.username, "manually_approves_followers": False, - "url": federation_utils.full_url( - reverse("federation:actors-detail", kwargs={"user__username": username}) - ), - "shared_inbox_url": federation_utils.full_url( - reverse("federation:actors-inbox", kwargs={"user__username": username}) + "fid": federation_utils.full_url( + reverse("federation:actors-detail", kwargs={"preferred_username": username}) ), + "shared_inbox_url": federation_models.get_shared_inbox_url(), "inbox_url": federation_utils.full_url( - reverse("federation:actors-inbox", kwargs={"user__username": username}) + reverse("federation:actors-inbox", kwargs={"preferred_username": username}) ), "outbox_url": federation_utils.full_url( - reverse("federation:actors-outbox", kwargs={"user__username": username}) + reverse("federation:actors-outbox", kwargs={"preferred_username": username}) + ), + "followers_url": federation_utils.full_url( + reverse( + "federation:actors-followers", kwargs={"preferred_username": username} + ) + ), + "following_url": federation_utils.full_url( + reverse( + "federation:actors-following", kwargs={"preferred_username": username} + ) ), } + + +def create_actor(user): + args = get_actor_data(user) + private, public = keys.get_key_pair() args["private_key"] = private.decode("utf-8") args["public_key"] = public.decode("utf-8") - return federation_models.Actor.objects.create(**args) + return federation_models.Actor.objects.create(user=user, **args) + + +@receiver(ldap_populate_user) +def init_ldap_user(sender, user, ldap_user, **kwargs): + if not user.actor: + user.actor = create_actor(user) @receiver(models.signals.post_save, sender=User) diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 2f2271584..bcacc3144 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -109,6 +109,16 @@ class UserReadSerializer(serializers.ModelSerializer): return o.get_permissions() +class MeSerializer(UserReadSerializer): + quota_status = serializers.SerializerMethodField() + + class Meta(UserReadSerializer.Meta): + fields = UserReadSerializer.Meta.fields + ["quota_status"] + + def get_quota_status(self, o): + return o.get_quota_status() if o.actor else 0 + + class PasswordResetSerializer(PRS): def get_email_options(self): return {"extra_email_context": {"funkwhale_url": settings.FUNKWHALE_URL}} diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index 20d63d788..3ca0c6b61 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -31,7 +31,7 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): @list_route(methods=["get"]) def me(self, request, *args, **kwargs): """Return information about the current user""" - serializer = serializers.UserReadSerializer(request.user) + serializer = serializers.MeSerializer(request.user) return Response(serializer.data) @detail_route(methods=["get", "post", "delete"], url_path="subsonic-token") diff --git a/api/install_os_dependencies.sh b/api/install_os_dependencies.sh index 91f3f7c3f..77ebc4a79 100755 --- a/api/install_os_dependencies.sh +++ b/api/install_os_dependencies.sh @@ -1,6 +1,8 @@ -#!/bin/bash +#!/bin/bash -ex -OS_REQUIREMENTS_FILENAME="requirements.apt" +script_path=$(dirname "$(realpath $0)") + +OS_REQUIREMENTS_FILENAME="$script_path/requirements.apt" # Handle call with wrong command function wrong_command() diff --git a/api/requirements.apt b/api/requirements.apt index 224ff955a..6e4db7a3b 100644 --- a/api/requirements.apt +++ b/api/requirements.apt @@ -6,3 +6,5 @@ libmagic-dev libpq-dev postgresql-client python3-dev +libldap2-dev +libsasl2-dev diff --git a/api/requirements.pac b/api/requirements.pac index 7e7cb8a0d..c173600a2 100644 --- a/api/requirements.pac +++ b/api/requirements.pac @@ -4,3 +4,5 @@ ffmpeg libjpeg-turbo libpqxx python +libldap +libsasl diff --git a/api/requirements/base.txt b/api/requirements/base.txt index bb441ac38..246525b99 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -49,7 +49,7 @@ mutagen>=1.39,<1.40 # Until this is merged django-taggit>=0.22,<0.23 # Until this is merged -git+https://github.com/EliotBerriot/PyMemoize.git@django +pymemoize==1.0.3 django-dynamic-preferences>=1.5,<1.6 pyacoustid>=1.1.5,<1.2 @@ -65,3 +65,7 @@ cryptography>=2,<3 # clone until the branch is merged and released upstream git+https://github.com/EliotBerriot/requests-http-signature.git@signature-header-support django-cleanup==2.1.0 + +# for LDAP authentication +python-ldap==3.1.0 +django-auth-ldap==1.7.0 diff --git a/api/requirements/local.txt b/api/requirements/local.txt index f11f976b8..c12f1ecb8 100644 --- a/api/requirements/local.txt +++ b/api/requirements/local.txt @@ -10,3 +10,4 @@ django-debug-toolbar>=1.9,<1.10 # improved REPL ipdb==0.8.1 black +profiling diff --git a/api/tests/activity/test_record.py b/api/tests/activity/test_record.py index 69d3a28cf..5b18a81ec 100644 --- a/api/tests/activity/test_record.py +++ b/api/tests/activity/test_record.py @@ -1,4 +1,3 @@ - from django.db import models from rest_framework import serializers diff --git a/api/tests/common/test_scripts.py b/api/tests/common/test_scripts.py index 40d9ea0a7..2004cfe57 100644 --- a/api/tests/common/test_scripts.py +++ b/api/tests/common/test_scripts.py @@ -2,6 +2,8 @@ import pytest from funkwhale_api.common import scripts from funkwhale_api.common.management.commands import script +from funkwhale_api.federation import models as federation_models +from funkwhale_api.music import models as music_models @pytest.fixture @@ -42,3 +44,218 @@ def test_django_permissions_to_user_permissions(factories, command): assert user2.permission_settings is False assert user2.permission_library is True assert user2.permission_federation is True + + +@pytest.mark.parametrize( + "open_api,expected_visibility", [(True, "everyone"), (False, "instance")] +) +def test_migrate_to_user_libraries_create_libraries( + factories, open_api, expected_visibility, stdout +): + user1 = factories["users.User"](with_actor=True) + user2 = factories["users.User"](with_actor=True) + + result = scripts.migrate_to_user_libraries.create_libraries(open_api, stdout) + + user1_library = user1.actor.libraries.get( + name="default", privacy_level=expected_visibility + ) + user2_library = user2.actor.libraries.get( + name="default", privacy_level=expected_visibility + ) + + assert result == {user1.pk: user1_library.pk, user2.pk: user2_library.pk} + + +def test_migrate_to_user_libraries_update_uploads(factories, stdout): + user1 = factories["users.User"](with_actor=True) + user2 = factories["users.User"](with_actor=True) + + library1 = factories["music.Library"](actor=user1.actor) + library2 = factories["music.Library"](actor=user2.actor) + + upload1 = factories["music.Upload"]() + upload2 = factories["music.Upload"]() + + # we delete libraries + upload1.library = None + upload2.library = None + upload1.save() + upload2.save() + + factories["music.ImportJob"](batch__submitted_by=user1, upload=upload1) + factories["music.ImportJob"](batch__submitted_by=user2, upload=upload2) + + libraries_by_user = {user1.pk: library1.pk, user2.pk: library2.pk} + + scripts.migrate_to_user_libraries.update_uploads(libraries_by_user, stdout) + + upload1.refresh_from_db() + upload2.refresh_from_db() + + assert upload1.library == library1 + assert upload1.import_status == "finished" + assert upload2.library == library2 + assert upload2.import_status == "finished" + + +@pytest.mark.parametrize( + "open_api,expected_visibility", [(True, "everyone"), (False, "instance")] +) +def test_migrate_to_user_libraries_without_jobs( + factories, open_api, expected_visibility, stdout +): + superuser = factories["users.User"](is_superuser=True, with_actor=True) + upload1 = factories["music.Upload"]() + upload2 = factories["music.Upload"]() + upload3 = factories["music.Upload"](audio_file=None) + + # we delete libraries + upload1.library = None + upload2.library = None + upload3.library = None + upload1.save() + upload2.save() + upload3.save() + + factories["music.ImportJob"](upload=upload2) + scripts.migrate_to_user_libraries.update_orphan_uploads(open_api, stdout) + + upload1.refresh_from_db() + upload2.refresh_from_db() + upload3.refresh_from_db() + + superuser_library = superuser.actor.libraries.get( + name="default", privacy_level=expected_visibility + ) + assert upload1.library == superuser_library + assert upload1.import_status == "finished" + # left untouched because they don't match filters + assert upload2.library is None + assert upload3.library is None + + +@pytest.mark.parametrize( + "model,args,path", + [ + ("music.Upload", {"library__actor__local": True}, "/federation/music/uploads/"), + ("music.Artist", {}, "/federation/music/artists/"), + ("music.Album", {}, "/federation/music/albums/"), + ("music.Track", {}, "/federation/music/tracks/"), + ], +) +def test_migrate_to_user_libraries_generate_fids( + factories, args, model, path, settings, stdout +): + template = "{}{}{}" + + objects = factories[model].create_batch(5, fid=None, **args) + klass = factories[model]._meta.model + + # we leave a fid on the first one, and set the others to None + existing_fid = objects[0].fid + base_path = existing_fid.replace(str(objects[0].uuid), "") + klass.objects.filter(pk__in=[o.pk for o in objects[1:]]).update(fid=None) + + scripts.migrate_to_user_libraries.set_fid(klass.objects.all(), path, stdout) + + for i, o in enumerate(objects): + o.refresh_from_db() + if i == 0: + assert o.fid == existing_fid + else: + assert o.fid == template.format(settings.FUNKWHALE_URL, path, o.uuid) + # we also ensure the path we insert match the one that is generated + # by the app on objects creation, as a safe guard for typos + assert base_path == o.fid.replace(str(o.uuid), "") + + +def test_migrate_to_user_libraries_update_actors_shared_inbox_url(factories, stdout): + local = factories["federation.Actor"](local=True, shared_inbox_url=None) + remote = factories["federation.Actor"](local=False, shared_inbox_url=None) + expected = federation_models.get_shared_inbox_url() + scripts.migrate_to_user_libraries.update_shared_inbox_url(stdout) + + local.refresh_from_db() + remote.refresh_from_db() + + assert local.shared_inbox_url == expected + assert remote.shared_inbox_url is None + + +@pytest.mark.parametrize("part", ["following", "followers"]) +def test_migrate_to_user_libraries_generate_actor_urls( + factories, part, settings, stdout +): + field = "{}_url".format(part) + ok = factories["users.User"]().create_actor() + local = factories["federation.Actor"](local=True, **{field: None}) + remote = factories["federation.Actor"](local=False, **{field: None}) + + assert getattr(local, field) is None + expected = "{}/federation/actors/{}/{}".format( + settings.FUNKWHALE_URL, local.preferred_username, part + ) + ok_url = getattr(ok, field) + + scripts.migrate_to_user_libraries.generate_actor_urls(part, stdout) + + ok.refresh_from_db() + local.refresh_from_db() + remote.refresh_from_db() + + # unchanged + assert getattr(ok, field) == ok_url + assert getattr(remote, field) is None + + assert getattr(local, field) == expected + assert expected.replace(local.preferred_username, "") == ok_url.replace( + ok.preferred_username, "" + ) + + +def test_migrate_to_users_libraries_command( + preferences, mocker, db, command, queryset_equal_queries +): + preferences["common__api_authentication_required"] = False + open_api = not preferences["common__api_authentication_required"] + create_libraries = mocker.patch.object( + scripts.migrate_to_user_libraries, + "create_libraries", + return_value={"hello": "world"}, + ) + update_uploads = mocker.patch.object( + scripts.migrate_to_user_libraries, "update_uploads" + ) + update_orphan_uploads = mocker.patch.object( + scripts.migrate_to_user_libraries, "update_orphan_uploads" + ) + set_fid = mocker.patch.object(scripts.migrate_to_user_libraries, "set_fid") + update_shared_inbox_url = mocker.patch.object( + scripts.migrate_to_user_libraries, "update_shared_inbox_url" + ) + generate_actor_urls = mocker.patch.object( + scripts.migrate_to_user_libraries, "generate_actor_urls" + ) + + scripts.migrate_to_user_libraries.main(command) + + create_libraries.assert_called_once_with(open_api, command.stdout) + update_uploads.assert_called_once_with({"hello": "world"}, command.stdout) + update_orphan_uploads.assert_called_once_with(open_api, command.stdout) + set_fid_params = [ + ( + music_models.Upload.objects.exclude(library__actor__user=None), + "/federation/music/uploads/", + ), + (music_models.Artist.objects.all(), "/federation/music/artists/"), + (music_models.Album.objects.all(), "/federation/music/albums/"), + (music_models.Track.objects.all(), "/federation/music/tracks/"), + ] + for qs, path in set_fid_params: + set_fid.assert_any_call(qs, path, command.stdout) + update_shared_inbox_url.assert_called_once_with(command.stdout) + # generate_actor_urls(part, stdout): + + for part in ["followers", "following"]: + generate_actor_urls.assert_any_call(part, command.stdout) diff --git a/api/tests/common/test_utils.py b/api/tests/common/test_utils.py new file mode 100644 index 000000000..28d757eb5 --- /dev/null +++ b/api/tests/common/test_utils.py @@ -0,0 +1,10 @@ +from funkwhale_api.common import utils + + +def test_chunk_queryset(factories): + actors = factories["federation.Actor"].create_batch(size=4) + queryset = actors[0].__class__.objects.all() + chunks = list(utils.chunk_queryset(queryset, 2)) + + assert list(chunks[0]) == actors[0:2] + assert list(chunks[1]) == actors[2:4] diff --git a/api/tests/conftest.py b/api/tests/conftest.py index fc7e11947..d880c8d6f 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -1,17 +1,23 @@ +import contextlib import datetime import io +import os import PIL import random import shutil import tempfile +import uuid +from faker.providers import internet as internet_provider import factory import pytest -import requests_mock + from django.contrib.auth.models import AnonymousUser from django.core.cache import cache as django_cache +from django.core.files import uploadedfile from django.utils import timezone from django.test import client +from django.db.models import QuerySet from dynamic_preferences.registries import global_preferences_registry from rest_framework import fields as rest_fields from rest_framework.test import APIClient, APIRequestFactory @@ -20,6 +26,62 @@ from funkwhale_api.activity import record from funkwhale_api.users.permissions import HasUserPermission +class FunkwhaleProvider(internet_provider.Provider): + """ + Our own faker data generator, since built-in ones are sometimes + not random enough + """ + + def federation_url(self, prefix=""): + def path_generator(): + return "{}/{}".format(prefix, uuid.uuid4()) + + domain = self.domain_name() + protocol = "https" + path = path_generator() + return "{}://{}/{}".format(protocol, domain, path) + + +factory.Faker.add_provider(FunkwhaleProvider) + + +@pytest.fixture +def queryset_equal_queries(): + """ + Unitting querysets is hard because we have to compare queries + by hand. Let's monkey patch querysets to do that for us. + """ + + def __eq__(self, other): + if isinstance(other, QuerySet): + return str(other.query) == str(self.query) + else: + return False + + setattr(QuerySet, "__eq__", __eq__) + yield __eq__ + delattr(QuerySet, "__eq__") + + +@pytest.fixture +def queryset_equal_list(): + """ + Unitting querysets is hard because we usually simply wants to ensure + a querysets contains the same objects as a list, let's monkey patch + querysets to to that for us. + """ + + def __eq__(self, other): + if isinstance(other, (list, tuple)): + return list(self) == list(other) + else: + return False + + setattr(QuerySet, "__eq__", __eq__) + yield __eq__ + delattr(QuerySet, "__eq__") + + @pytest.fixture(scope="session", autouse=True) def factories_autodiscover(): from django.apps import apps @@ -209,14 +271,22 @@ def media_root(settings): shutil.rmtree(tmp_dir) -@pytest.fixture -def r_mock(): +@pytest.fixture(autouse=True) +def disabled_musicbrainz(mocker): + # we ensure no music brainz requests gets out + yield mocker.patch( + "musicbrainzngs.musicbrainz._safe_read", + side_effect=Exception("Disabled network calls"), + ) + + +@pytest.fixture(autouse=True) +def r_mock(requests_mock): """ Returns a requests_mock.mock() object you can use to mock HTTP calls made using python-requests """ - with requests_mock.mock() as m: - yield m + yield requests_mock @pytest.fixture @@ -272,3 +342,43 @@ def avatar(): f.seek(0) yield f f.close() + + +@pytest.fixture() +def audio_file(): + data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "music") + path = os.path.join(data_dir, "test.ogg") + assert os.path.exists(path) + with open(path, "rb") as f: + yield f + + +@pytest.fixture() +def uploaded_audio_file(audio_file): + yield uploadedfile.SimpleUploadedFile( + name=audio_file.name, content=audio_file.read() + ) + + +@pytest.fixture() +def temp_signal(mocker): + """ + Connect a temporary handler to a given signal. This is helpful to validate + a signal is dispatched properly, without mocking. + """ + + @contextlib.contextmanager + def connect(signal): + stub = mocker.stub() + signal.connect(stub) + try: + yield stub + finally: + signal.disconnect(stub) + + return connect + + +@pytest.fixture() +def stdout(): + yield io.StringIO() diff --git a/api/tests/favorites/test_favorites.py b/api/tests/favorites/test_favorites.py index 0b99c9340..6ac244c69 100644 --- a/api/tests/favorites/test_favorites.py +++ b/api/tests/favorites/test_favorites.py @@ -35,6 +35,7 @@ def test_user_can_get_his_favorites(api_request, factories, logged_in_client, cl "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"), } ] + expected[0]["track"]["is_playable"] = False assert response.status_code == 200 assert response.data["results"] == expected diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py index 9c7bb70ec..9604582e5 100644 --- a/api/tests/federation/test_activity.py +++ b/api/tests/federation/test_activity.py @@ -1,39 +1,345 @@ +import pytest +import uuid -from funkwhale_api.federation import activity, serializers +from django.db.models import Q +from django.urls import reverse + +from funkwhale_api.federation import ( + activity, + models, + api_serializers, + serializers, + tasks, +) -def test_deliver(factories, r_mock, mocker, settings): - settings.CELERY_TASK_ALWAYS_EAGER = True - to = factories["federation.Actor"]() - mocker.patch("funkwhale_api.federation.actors.get_actor", return_value=to) - sender = factories["federation.Actor"]() - ac = { - "id": "http://test.federation/activity", - "type": "Create", - "actor": sender.url, - "object": { - "id": "http://test.federation/note", - "type": "Note", - "content": "Hello", - }, +def test_receive_validates_basic_attributes_and_stores_activity(factories, now, mocker): + mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit") + local_to_actor = factories["users.User"]().create_actor() + local_cc_actor = factories["users.User"]().create_actor() + remote_actor = factories["federation.Actor"]() + a = { + "@context": [], + "actor": remote_actor.fid, + "type": "Noop", + "id": "https://test.activity", + "to": [local_to_actor.fid, remote_actor.fid], + "cc": [local_cc_actor.fid, activity.PUBLIC_ADDRESS], } - r_mock.post(to.inbox_url) + copy = activity.receive(activity=a, on_behalf_of=remote_actor) - activity.deliver(ac, to=[to.url], on_behalf_of=sender) - request = r_mock.request_history[0] - - assert r_mock.called is True - assert r_mock.call_count == 1 - assert request.url == to.inbox_url - assert request.headers["content-type"] == "application/activity+json" - - -def test_accept_follow(mocker, factories): - deliver = mocker.patch("funkwhale_api.federation.activity.deliver") - follow = factories["federation.Follow"](approved=None) - expected_accept = serializers.AcceptFollowSerializer(follow).data - activity.accept_follow(follow) - deliver.assert_called_once_with( - expected_accept, to=[follow.actor.url], on_behalf_of=follow.target + assert copy.payload == a + assert copy.creation_date >= now + assert copy.actor == remote_actor + assert copy.fid == a["id"] + assert copy.type == "Noop" + mocked_dispatch.assert_called_once_with( + tasks.dispatch_inbox.delay, activity_id=copy.pk ) + + assert models.InboxItem.objects.count() == 2 + for actor, t in [(local_to_actor, "to"), (local_cc_actor, "cc")]: + ii = models.InboxItem.objects.get(actor=actor) + assert ii.type == t + assert ii.activity == copy + assert ii.is_read is False + + +def test_get_actors_from_audience_urls(settings, db): + settings.FEDERATION_HOSTNAME = "federation.hostname" + library_uuid1 = uuid.uuid4() + library_uuid2 = uuid.uuid4() + + urls = [ + "https://wrong.url", + "https://federation.hostname" + + reverse("federation:actors-detail", kwargs={"preferred_username": "kevin"}), + "https://federation.hostname" + + reverse("federation:actors-detail", kwargs={"preferred_username": "alice"}), + "https://federation.hostname" + + reverse("federation:actors-detail", kwargs={"preferred_username": "bob"}), + "https://federation.hostname" + + reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid1}), + "https://federation.hostname" + + reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid2}), + activity.PUBLIC_ADDRESS, + ] + followed_query = Q(target__followers_url=urls[0]) + for url in urls[1:-1]: + followed_query |= Q(target__followers_url=url) + actor_follows = models.Follow.objects.filter(followed_query, approved=True) + library_follows = models.LibraryFollow.objects.filter(followed_query, approved=True) + expected = models.Actor.objects.filter( + Q(fid__in=urls[0:-1]) + | Q(pk__in=actor_follows.values_list("actor", flat=True)) + | Q(pk__in=library_follows.values_list("actor", flat=True)) + ) + assert str(activity.get_actors_from_audience(urls).query) == str(expected.query) + + +def test_get_inbox_urls(factories): + a1 = factories["federation.Actor"]( + shared_inbox_url=None, inbox_url="https://a1.inbox" + ) + a2 = factories["federation.Actor"]( + shared_inbox_url="https://shared.inbox", inbox_url="https://a2.inbox" + ) + factories["federation.Actor"]( + shared_inbox_url="https://shared.inbox", inbox_url="https://a3.inbox" + ) + + expected = sorted(set([a1.inbox_url, a2.shared_inbox_url])) + + assert activity.get_inbox_urls(a1.__class__.objects.all()) == expected + + +def test_receive_invalid_data(factories): + remote_actor = factories["federation.Actor"]() + a = {"@context": [], "actor": remote_actor.fid, "id": "https://test.activity"} + + with pytest.raises(serializers.serializers.ValidationError): + activity.receive(activity=a, on_behalf_of=remote_actor) + + +def test_receive_actor_mismatch(factories): + remote_actor = factories["federation.Actor"]() + a = { + "@context": [], + "type": "Noop", + "actor": "https://hello", + "id": "https://test.activity", + } + + with pytest.raises(serializers.serializers.ValidationError): + activity.receive(activity=a, on_behalf_of=remote_actor) + + +def test_inbox_routing(factories, mocker): + object = factories["music.Artist"]() + target = factories["music.Artist"]() + router = activity.InboxRouter() + a = factories["federation.Activity"](type="Follow") + + handler_payload = {} + handler_context = {} + + def handler(payload, context): + handler_payload.update(payload) + handler_context.update(context) + return {"target": target, "object": object} + + router.connect({"type": "Follow"}, handler) + + good_message = {"type": "Follow"} + router.dispatch(good_message, context={"activity": a}) + + assert handler_payload == good_message + assert handler_context == {"activity": a} + + a.refresh_from_db() + + assert a.object == object + assert a.target == target + + +def test_inbox_routing_send_to_channel(factories, mocker): + group_send = mocker.patch("funkwhale_api.common.channels.group_send") + a = factories["federation.Activity"](type="Follow") + ii = factories["federation.InboxItem"](actor__local=True) + + router = activity.InboxRouter() + handler = mocker.stub() + router.connect({"type": "Follow"}, handler) + good_message = {"type": "Follow"} + router.dispatch( + good_message, context={"activity": a, "inbox_items": ii.__class__.objects.all()} + ) + + ii.refresh_from_db() + + group_send.assert_called_once_with( + "user.{}.inbox".format(ii.actor.user.pk), + { + "type": "event.send", + "text": "", + "data": { + "type": "inbox.item_added", + "item": api_serializers.InboxItemSerializer(ii).data, + }, + }, + ) + + +@pytest.mark.parametrize( + "route,payload,expected", + [ + ({"type": "Follow"}, {"type": "Follow"}, True), + ({"type": "Follow"}, {"type": "Noop"}, False), + ({"type": "Follow"}, {"type": "Follow", "id": "https://hello"}, True), + ( + {"type": "Create", "object.type": "Audio"}, + {"type": "Create", "object": {"type": "Note"}}, + False, + ), + ( + {"type": "Create", "object.type": "Audio"}, + {"type": "Create", "object": {"type": "Audio"}}, + True, + ), + ], +) +def test_route_matching(route, payload, expected): + assert activity.match_route(route, payload) is expected + + +def test_outbox_router_dispatch(mocker, factories, now): + router = activity.OutboxRouter() + actor = factories["federation.Actor"]() + r1 = factories["federation.Actor"]() + r2 = factories["federation.Actor"]() + mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit") + + def handler(context): + yield { + "payload": { + "type": "Noop", + "actor": actor.fid, + "summary": context["summary"], + "to": [r1], + "cc": [r2, activity.PUBLIC_ADDRESS], + }, + "actor": actor, + } + + expected_deliveries_url = activity.get_inbox_urls( + models.Actor.objects.filter(pk__in=[r1.pk, r2.pk]) + ) + router.connect({"type": "Noop"}, handler) + activities = router.dispatch({"type": "Noop"}, {"summary": "hello"}) + a = activities[0] + + mocked_dispatch.assert_called_once_with( + tasks.dispatch_outbox.delay, activity_id=a.pk + ) + + assert a.payload == { + "type": "Noop", + "actor": actor.fid, + "summary": "hello", + "to": [r1.fid], + "cc": [r2.fid, activity.PUBLIC_ADDRESS], + } + assert a.actor == actor + assert a.creation_date >= now + assert a.uuid is not None + + assert a.deliveries.count() == 2 + for url in expected_deliveries_url: + delivery = a.deliveries.get(inbox_url=url) + assert delivery.is_delivered is False + + +def test_prepare_deliveries_and_inbox_items(factories): + local_actor1 = factories["federation.Actor"]( + local=True, shared_inbox_url="https://testlocal.inbox" + ) + local_actor2 = factories["federation.Actor"]( + local=True, shared_inbox_url=local_actor1.shared_inbox_url + ) + local_actor3 = factories["federation.Actor"](local=True, shared_inbox_url=None) + + remote_actor1 = factories["federation.Actor"]( + shared_inbox_url="https://testremote.inbox" + ) + remote_actor2 = factories["federation.Actor"]( + shared_inbox_url=remote_actor1.shared_inbox_url + ) + remote_actor3 = factories["federation.Actor"](shared_inbox_url=None) + + library = factories["music.Library"]() + library_follower_local = factories["federation.LibraryFollow"]( + target=library, actor__local=True, approved=True + ).actor + library_follower_remote = factories["federation.LibraryFollow"]( + target=library, actor__local=False, approved=True + ).actor + # follow not approved + factories["federation.LibraryFollow"]( + target=library, actor__local=False, approved=False + ) + + followed_actor = factories["federation.Actor"]() + actor_follower_local = factories["federation.Follow"]( + target=followed_actor, actor__local=True, approved=True + ).actor + actor_follower_remote = factories["federation.Follow"]( + target=followed_actor, actor__local=False, approved=True + ).actor + # follow not approved + factories["federation.Follow"]( + target=followed_actor, actor__local=False, approved=False + ) + + recipients = [ + local_actor1, + local_actor2, + local_actor3, + remote_actor1, + remote_actor2, + remote_actor3, + activity.PUBLIC_ADDRESS, + {"type": "followers", "target": library}, + {"type": "followers", "target": followed_actor}, + ] + + inbox_items, deliveries, urls = activity.prepare_deliveries_and_inbox_items( + recipients, "to" + ) + expected_inbox_items = sorted( + [ + models.InboxItem(actor=local_actor1, type="to"), + models.InboxItem(actor=local_actor2, type="to"), + models.InboxItem(actor=local_actor3, type="to"), + models.InboxItem(actor=library_follower_local, type="to"), + models.InboxItem(actor=actor_follower_local, type="to"), + ], + key=lambda v: v.actor.pk, + ) + + expected_deliveries = sorted( + [ + models.Delivery(inbox_url=remote_actor1.shared_inbox_url), + models.Delivery(inbox_url=remote_actor3.inbox_url), + models.Delivery(inbox_url=library_follower_remote.inbox_url), + models.Delivery(inbox_url=actor_follower_remote.inbox_url), + ], + key=lambda v: v.inbox_url, + ) + + expected_urls = [ + local_actor1.fid, + local_actor2.fid, + local_actor3.fid, + remote_actor1.fid, + remote_actor2.fid, + remote_actor3.fid, + activity.PUBLIC_ADDRESS, + library.followers_url, + followed_actor.followers_url, + ] + + assert urls == expected_urls + assert len(expected_inbox_items) == len(inbox_items) + assert len(expected_deliveries) == len(deliveries) + + for delivery, expected_delivery in zip( + sorted(deliveries, key=lambda v: v.inbox_url), expected_deliveries + ): + assert delivery.inbox_url == expected_delivery.inbox_url + + for inbox_item, expected_inbox_item in zip( + sorted(inbox_items, key=lambda v: v.actor.pk), expected_inbox_items + ): + assert inbox_item.actor == expected_inbox_item.actor + assert inbox_item.type == "to" diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 1244533de..a416cd78f 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -1,12 +1,4 @@ -import pendulum -import pytest -from django.urls import reverse -from django.utils import timezone -from rest_framework import exceptions - -from funkwhale_api.federation import actors, models, serializers, utils -from funkwhale_api.music import models as music_models -from funkwhale_api.music import tasks as music_tasks +from funkwhale_api.federation import actors, serializers def test_actor_fetching(r_mock): @@ -25,8 +17,8 @@ def test_actor_fetching(r_mock): def test_get_actor(factories, r_mock): actor = factories["federation.Actor"].build() payload = serializers.ActorSerializer(actor).data - r_mock.get(actor.url, json=payload) - new_actor = actors.get_actor(actor.url) + r_mock.get(actor.fid, json=payload) + new_actor = actors.get_actor(actor.fid) assert new_actor.pk is not None assert serializers.ActorSerializer(new_actor).data == payload @@ -36,7 +28,7 @@ def test_get_actor_use_existing(factories, preferences, mocker): preferences["federation__actor_fetch_delay"] = 60 actor = factories["federation.Actor"]() get_data = mocker.patch("funkwhale_api.federation.actors.get_actor_data") - new_actor = actors.get_actor(actor.url) + new_actor = actors.get_actor(actor.fid) assert new_actor == actor get_data.assert_not_called() @@ -49,465 +41,8 @@ def test_get_actor_refresh(factories, preferences, mocker): # actor changed their username in the meantime payload["preferredUsername"] = "New me" mocker.patch("funkwhale_api.federation.actors.get_actor_data", return_value=payload) - new_actor = actors.get_actor(actor.url) + new_actor = actors.get_actor(actor.fid) assert new_actor == actor assert new_actor.last_fetch_date > actor.last_fetch_date assert new_actor.preferred_username == "New me" - - -def test_get_library(db, settings, mocker): - mocker.patch( - "funkwhale_api.federation.keys.get_key_pair", - return_value=(b"private", b"public"), - ) - expected = { - "preferred_username": "library", - "domain": settings.FEDERATION_HOSTNAME, - "type": "Person", - "name": "{}'s library".format(settings.FEDERATION_HOSTNAME), - "manually_approves_followers": True, - "public_key": "public", - "url": utils.full_url( - reverse("federation:instance-actors-detail", kwargs={"actor": "library"}) - ), - "shared_inbox_url": utils.full_url( - reverse("federation:instance-actors-inbox", kwargs={"actor": "library"}) - ), - "inbox_url": utils.full_url( - reverse("federation:instance-actors-inbox", kwargs={"actor": "library"}) - ), - "outbox_url": utils.full_url( - reverse("federation:instance-actors-outbox", kwargs={"actor": "library"}) - ), - "summary": "Bot account to federate with {}'s library".format( - settings.FEDERATION_HOSTNAME - ), - } - actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - for key, value in expected.items(): - assert getattr(actor, key) == value - - -def test_get_test(db, mocker, settings): - mocker.patch( - "funkwhale_api.federation.keys.get_key_pair", - return_value=(b"private", b"public"), - ) - expected = { - "preferred_username": "test", - "domain": settings.FEDERATION_HOSTNAME, - "type": "Person", - "name": "{}'s test account".format(settings.FEDERATION_HOSTNAME), - "manually_approves_followers": False, - "public_key": "public", - "url": utils.full_url( - reverse("federation:instance-actors-detail", kwargs={"actor": "test"}) - ), - "shared_inbox_url": utils.full_url( - reverse("federation:instance-actors-inbox", kwargs={"actor": "test"}) - ), - "inbox_url": utils.full_url( - reverse("federation:instance-actors-inbox", kwargs={"actor": "test"}) - ), - "outbox_url": utils.full_url( - reverse("federation:instance-actors-outbox", kwargs={"actor": "test"}) - ), - "summary": "Bot account to test federation with {}. Send me /ping and I'll answer you.".format( - settings.FEDERATION_HOSTNAME - ), - } - actor = actors.SYSTEM_ACTORS["test"].get_actor_instance() - for key, value in expected.items(): - assert getattr(actor, key) == value - - -def test_test_get_outbox(): - expected = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {}, - ], - "id": utils.full_url( - reverse("federation:instance-actors-outbox", kwargs={"actor": "test"}) - ), - "type": "OrderedCollection", - "totalItems": 0, - "orderedItems": [], - } - - data = actors.SYSTEM_ACTORS["test"].get_outbox({}, actor=None) - - assert data == expected - - -def test_test_post_inbox_requires_authenticated_actor(): - with pytest.raises(exceptions.PermissionDenied): - actors.SYSTEM_ACTORS["test"].post_inbox({}, actor=None) - - -def test_test_post_outbox_validates_actor(nodb_factories): - actor = nodb_factories["federation.Actor"]() - data = {"actor": "noop"} - with pytest.raises(exceptions.ValidationError) as exc_info: - actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor) - msg = "The actor making the request do not match" - assert msg in exc_info.value - - -def test_test_post_inbox_handles_create_note(settings, mocker, factories): - deliver = mocker.patch("funkwhale_api.federation.activity.deliver") - actor = factories["federation.Actor"]() - now = timezone.now() - mocker.patch("django.utils.timezone.now", return_value=now) - data = { - "actor": actor.url, - "type": "Create", - "id": "http://test.federation/activity", - "object": { - "type": "Note", - "id": "http://test.federation/object", - "content": "

@mention /ping

", - }, - } - test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance() - expected_note = factories["federation.Note"]( - id="https://test.federation/activities/note/{}".format(now.timestamp()), - content="Pong!", - published=now.isoformat(), - inReplyTo=data["object"]["id"], - cc=[], - summary=None, - sensitive=False, - attributedTo=test_actor.url, - attachment=[], - to=[actor.url], - url="https://{}/activities/note/{}".format( - settings.FEDERATION_HOSTNAME, now.timestamp() - ), - tag=[{"href": actor.url, "name": actor.mention_username, "type": "Mention"}], - ) - expected_activity = { - "@context": serializers.AP_CONTEXT, - "actor": test_actor.url, - "id": "https://{}/activities/note/{}/activity".format( - settings.FEDERATION_HOSTNAME, now.timestamp() - ), - "to": actor.url, - "type": "Create", - "published": now.isoformat(), - "object": expected_note, - "cc": [], - } - actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor) - deliver.assert_called_once_with( - expected_activity, - to=[actor.url], - on_behalf_of=actors.SYSTEM_ACTORS["test"].get_actor_instance(), - ) - - -def test_getting_actor_instance_persists_in_db(db): - test = actors.SYSTEM_ACTORS["test"].get_actor_instance() - from_db = models.Actor.objects.get(url=test.url) - - for f in test._meta.fields: - assert getattr(from_db, f.name) == getattr(test, f.name) - - -@pytest.mark.parametrize( - "username,domain,expected", - [("test", "wrongdomain.com", False), ("notsystem", "", False), ("test", "", True)], -) -def test_actor_is_system(username, domain, expected, nodb_factories, settings): - if not domain: - domain = settings.FEDERATION_HOSTNAME - - actor = nodb_factories["federation.Actor"]( - preferred_username=username, domain=domain - ) - assert actor.is_system is expected - - -@pytest.mark.parametrize( - "username,domain,expected", - [ - ("test", "wrongdomain.com", None), - ("notsystem", "", None), - ("test", "", actors.SYSTEM_ACTORS["test"]), - ], -) -def test_actor_system_conf(username, domain, expected, nodb_factories, settings): - if not domain: - domain = settings.FEDERATION_HOSTNAME - actor = nodb_factories["federation.Actor"]( - preferred_username=username, domain=domain - ) - assert actor.system_conf == expected - - -@pytest.mark.parametrize("value", [False, True]) -def test_library_actor_manually_approves_based_on_preference(value, preferences): - preferences["federation__music_needs_approval"] = value - library_conf = actors.SYSTEM_ACTORS["library"] - assert library_conf.manually_approves_followers is value - - -def test_system_actor_handle(mocker, nodb_factories): - handler = mocker.patch("funkwhale_api.federation.actors.TestActor.handle_create") - actor = nodb_factories["federation.Actor"]() - activity = nodb_factories["federation.Activity"](type="Create", actor=actor.url) - serializer = serializers.ActivitySerializer(data=activity) - assert serializer.is_valid() - actors.SYSTEM_ACTORS["test"].handle(activity, actor) - handler.assert_called_once_with(activity, actor) - - -def test_test_actor_handles_follow(settings, mocker, factories): - deliver = mocker.patch("funkwhale_api.federation.activity.deliver") - actor = factories["federation.Actor"]() - accept_follow = mocker.patch("funkwhale_api.federation.activity.accept_follow") - test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance() - data = { - "actor": actor.url, - "type": "Follow", - "id": "http://test.federation/user#follows/267", - "object": test_actor.url, - } - actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor) - follow = models.Follow.objects.get(target=test_actor, approved=True) - follow_back = models.Follow.objects.get(actor=test_actor, approved=None) - accept_follow.assert_called_once_with(follow) - deliver.assert_called_once_with( - serializers.FollowSerializer(follow_back).data, - on_behalf_of=test_actor, - to=[actor.url], - ) - - -def test_test_actor_handles_undo_follow(settings, mocker, factories): - deliver = mocker.patch("funkwhale_api.federation.activity.deliver") - test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance() - follow = factories["federation.Follow"](target=test_actor) - reverse_follow = factories["federation.Follow"]( - actor=test_actor, target=follow.actor - ) - follow_serializer = serializers.FollowSerializer(follow) - reverse_follow_serializer = serializers.FollowSerializer(reverse_follow) - undo = { - "@context": serializers.AP_CONTEXT, - "type": "Undo", - "id": follow_serializer.data["id"] + "/undo", - "actor": follow.actor.url, - "object": follow_serializer.data, - } - expected_undo = { - "@context": serializers.AP_CONTEXT, - "type": "Undo", - "id": reverse_follow_serializer.data["id"] + "/undo", - "actor": reverse_follow.actor.url, - "object": reverse_follow_serializer.data, - } - - actors.SYSTEM_ACTORS["test"].post_inbox(undo, actor=follow.actor) - deliver.assert_called_once_with( - expected_undo, to=[follow.actor.url], on_behalf_of=test_actor - ) - - assert models.Follow.objects.count() == 0 - - -def test_library_actor_handles_follow_manual_approval(preferences, mocker, factories): - preferences["federation__music_needs_approval"] = True - actor = factories["federation.Actor"]() - now = timezone.now() - mocker.patch("django.utils.timezone.now", return_value=now) - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - data = { - "actor": actor.url, - "type": "Follow", - "id": "http://test.federation/user#follows/267", - "object": library_actor.url, - } - - library_actor.system_conf.post_inbox(data, actor=actor) - follow = library_actor.received_follows.first() - - assert follow.actor == actor - assert follow.approved is None - - -def test_library_actor_handles_follow_auto_approval(preferences, mocker, factories): - preferences["federation__music_needs_approval"] = False - actor = factories["federation.Actor"]() - mocker.patch("funkwhale_api.federation.activity.accept_follow") - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - data = { - "actor": actor.url, - "type": "Follow", - "id": "http://test.federation/user#follows/267", - "object": library_actor.url, - } - library_actor.system_conf.post_inbox(data, actor=actor) - - follow = library_actor.received_follows.first() - - assert follow.actor == actor - assert follow.approved is True - - -def test_library_actor_handles_accept(mocker, factories): - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - actor = factories["federation.Actor"]() - pending_follow = factories["federation.Follow"]( - actor=library_actor, target=actor, approved=None - ) - serializer = serializers.AcceptFollowSerializer(pending_follow) - library_actor.system_conf.post_inbox(serializer.data, actor=actor) - - pending_follow.refresh_from_db() - - assert pending_follow.approved is True - - -def test_library_actor_handle_create_audio_no_library(mocker, factories): - # when we receive inbox create audio, we should not do anything - # if we don't have a configured library matching the sender - mocked_create = mocker.patch( - "funkwhale_api.federation.serializers.AudioSerializer.create" - ) - actor = factories["federation.Actor"]() - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - data = { - "actor": actor.url, - "type": "Create", - "id": "http://test.federation/audio/create", - "object": { - "id": "https://batch.import", - "type": "Collection", - "totalItems": 2, - "items": factories["federation.Audio"].create_batch(size=2), - }, - } - library_actor.system_conf.post_inbox(data, actor=actor) - - mocked_create.assert_not_called() - models.LibraryTrack.objects.count() == 0 - - -def test_library_actor_handle_create_audio_no_library_enabled(mocker, factories): - # when we receive inbox create audio, we should not do anything - # if we don't have an enabled library - mocked_create = mocker.patch( - "funkwhale_api.federation.serializers.AudioSerializer.create" - ) - disabled_library = factories["federation.Library"](federation_enabled=False) - actor = disabled_library.actor - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - data = { - "actor": actor.url, - "type": "Create", - "id": "http://test.federation/audio/create", - "object": { - "id": "https://batch.import", - "type": "Collection", - "totalItems": 2, - "items": factories["federation.Audio"].create_batch(size=2), - }, - } - library_actor.system_conf.post_inbox(data, actor=actor) - - mocked_create.assert_not_called() - models.LibraryTrack.objects.count() == 0 - - -def test_library_actor_handle_create_audio(mocker, factories): - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - remote_library = factories["federation.Library"](federation_enabled=True) - - data = { - "actor": remote_library.actor.url, - "type": "Create", - "id": "http://test.federation/audio/create", - "object": { - "id": "https://batch.import", - "type": "Collection", - "totalItems": 2, - "items": factories["federation.Audio"].create_batch(size=2), - }, - } - - library_actor.system_conf.post_inbox(data, actor=remote_library.actor) - - lts = list(remote_library.tracks.order_by("id")) - - assert len(lts) == 2 - - for i, a in enumerate(data["object"]["items"]): - lt = lts[i] - assert lt.pk is not None - assert lt.url == a["id"] - assert lt.library == remote_library - assert lt.audio_url == a["url"]["href"] - assert lt.audio_mimetype == a["url"]["mediaType"] - assert lt.metadata == a["metadata"] - assert lt.title == a["metadata"]["recording"]["title"] - assert lt.artist_name == a["metadata"]["artist"]["name"] - assert lt.album_title == a["metadata"]["release"]["title"] - assert lt.published_date == pendulum.parse(a["published"]) - - -def test_library_actor_handle_create_audio_autoimport(mocker, factories): - mocked_import = mocker.patch("funkwhale_api.common.utils.on_commit") - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - remote_library = factories["federation.Library"]( - federation_enabled=True, autoimport=True - ) - - data = { - "actor": remote_library.actor.url, - "type": "Create", - "id": "http://test.federation/audio/create", - "object": { - "id": "https://batch.import", - "type": "Collection", - "totalItems": 2, - "items": factories["federation.Audio"].create_batch(size=2), - }, - } - - library_actor.system_conf.post_inbox(data, actor=remote_library.actor) - - lts = list(remote_library.tracks.order_by("id")) - - assert len(lts) == 2 - - for i, a in enumerate(data["object"]["items"]): - lt = lts[i] - assert lt.pk is not None - assert lt.url == a["id"] - assert lt.library == remote_library - assert lt.audio_url == a["url"]["href"] - assert lt.audio_mimetype == a["url"]["mediaType"] - assert lt.metadata == a["metadata"] - assert lt.title == a["metadata"]["recording"]["title"] - assert lt.artist_name == a["metadata"]["artist"]["name"] - assert lt.album_title == a["metadata"]["release"]["title"] - assert lt.published_date == pendulum.parse(a["published"]) - - batch = music_models.ImportBatch.objects.latest("id") - - assert batch.jobs.count() == len(lts) - assert batch.source == "federation" - assert batch.submitted_by is None - - for i, job in enumerate(batch.jobs.order_by("id")): - lt = lts[i] - assert job.library_track == lt - assert job.mbid == lt.mbid - assert job.source == lt.url - - mocked_import.assert_any_call( - music_tasks.import_job_run.delay, import_job_id=job.pk, use_acoustid=False - ) diff --git a/api/tests/federation/test_api_filters.py b/api/tests/federation/test_api_filters.py new file mode 100644 index 000000000..c6e70b617 --- /dev/null +++ b/api/tests/federation/test_api_filters.py @@ -0,0 +1,9 @@ +from funkwhale_api.federation import filters +from funkwhale_api.federation import models + + +def test_inbox_item_filter_before(factories): + expected = models.InboxItem.objects.filter(pk__lte=12) + f = filters.InboxItemFilter({"before": 12}, queryset=models.InboxItem.objects.all()) + + assert str(f.qs.query) == str(expected.query) diff --git a/api/tests/federation/test_api_serializers.py b/api/tests/federation/test_api_serializers.py new file mode 100644 index 000000000..2eacda1a8 --- /dev/null +++ b/api/tests/federation/test_api_serializers.py @@ -0,0 +1,86 @@ +import pytest + +from funkwhale_api.federation import api_serializers +from funkwhale_api.federation import serializers + + +def test_library_serializer(factories): + library = factories["music.Library"](uploads_count=5678) + expected = { + "fid": library.fid, + "uuid": str(library.uuid), + "actor": serializers.APIActorSerializer(library.actor).data, + "name": library.name, + "description": library.description, + "creation_date": library.creation_date.isoformat().split("+")[0] + "Z", + "uploads_count": library.uploads_count, + "privacy_level": library.privacy_level, + "follow": None, + "latest_scan": None, + } + + serializer = api_serializers.LibrarySerializer(library) + + assert serializer.data == expected + + +def test_library_serializer_latest_scan(factories): + library = factories["music.Library"](uploads_count=5678) + scan = factories["music.LibraryScan"](library=library) + setattr(library, "latest_scans", [scan]) + expected = api_serializers.LibraryScanSerializer(scan).data + serializer = api_serializers.LibrarySerializer(library) + + assert serializer.data["latest_scan"] == expected + + +def test_library_serializer_with_follow(factories): + library = factories["music.Library"](uploads_count=5678) + follow = factories["federation.LibraryFollow"](target=library) + + setattr(library, "_follows", [follow]) + expected = { + "fid": library.fid, + "uuid": str(library.uuid), + "actor": serializers.APIActorSerializer(library.actor).data, + "name": library.name, + "description": library.description, + "creation_date": library.creation_date.isoformat().split("+")[0] + "Z", + "uploads_count": library.uploads_count, + "privacy_level": library.privacy_level, + "follow": api_serializers.NestedLibraryFollowSerializer(follow).data, + "latest_scan": None, + } + + serializer = api_serializers.LibrarySerializer(library) + + assert serializer.data == expected + + +def test_library_follow_serializer_validates_existing_follow(factories): + follow = factories["federation.LibraryFollow"]() + serializer = api_serializers.LibraryFollowSerializer( + data={"target": follow.target.uuid}, context={"actor": follow.actor} + ) + + assert serializer.is_valid() is False + assert "target" in serializer.errors + + +def test_library_follow_serializer_do_not_allow_own_library(factories): + actor = factories["federation.Actor"]() + library = factories["music.Library"](actor=actor) + serializer = api_serializers.LibraryFollowSerializer(context={"actor": actor}) + + with pytest.raises(api_serializers.serializers.ValidationError) as e: + serializer.validate_target(library) + assert "own library" in str(e) + + +def test_manage_upload_action_read(factories): + ii = factories["federation.InboxItem"]() + s = api_serializers.InboxItemActionSerializer(queryset=None) + + s.handle_read(ii.__class__.objects.all()) + + assert ii.__class__.objects.filter(is_read=False).count() == 0 diff --git a/api/tests/federation/test_api_views.py b/api/tests/federation/test_api_views.py new file mode 100644 index 000000000..c2d695184 --- /dev/null +++ b/api/tests/federation/test_api_views.py @@ -0,0 +1,166 @@ +import pytest + +from django.urls import reverse + +from funkwhale_api.federation import api_serializers +from funkwhale_api.federation import serializers +from funkwhale_api.federation import views + + +def test_user_can_list_their_library_follows(factories, logged_in_api_client): + # followed by someont else + factories["federation.LibraryFollow"]() + follow = factories["federation.LibraryFollow"]( + actor__user=logged_in_api_client.user + ) + url = reverse("api:v1:federation:library-follows-list") + response = logged_in_api_client.get(url) + + assert response.data["count"] == 1 + assert response.data["results"][0]["uuid"] == str(follow.uuid) + + +def test_user_can_fetch_library_using_url(mocker, factories, logged_in_api_client): + library = factories["music.Library"]() + mocked_retrieve = mocker.patch( + "funkwhale_api.federation.utils.retrieve", return_value=library + ) + url = reverse("api:v1:federation:libraries-fetch") + response = logged_in_api_client.post(url, {"fid": library.fid}) + assert mocked_retrieve.call_count == 1 + args = mocked_retrieve.call_args + assert args[0] == (library.fid,) + assert args[1]["queryset"].model == views.MusicLibraryViewSet.queryset.model + assert args[1]["serializer_class"] == serializers.LibrarySerializer + assert response.status_code == 200 + assert response.data["results"] == [api_serializers.LibrarySerializer(library).data] + + +def test_user_can_schedule_library_scan(mocker, factories, logged_in_api_client): + actor = logged_in_api_client.user.create_actor() + library = factories["music.Library"](privacy_level="everyone") + + schedule_scan = mocker.patch( + "funkwhale_api.music.models.Library.schedule_scan", return_value=True + ) + url = reverse("api:v1:federation:libraries-scan", kwargs={"uuid": library.uuid}) + + response = logged_in_api_client.post(url) + + assert response.status_code == 200 + + schedule_scan.assert_called_once_with(actor=actor) + + +def test_can_follow_library(factories, logged_in_api_client, mocker): + dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch") + actor = logged_in_api_client.user.create_actor() + library = factories["music.Library"]() + url = reverse("api:v1:federation:library-follows-list") + response = logged_in_api_client.post(url, {"target": library.uuid}) + + assert response.status_code == 201 + + follow = library.received_follows.latest("id") + + assert follow.approved is None + assert follow.actor == actor + + dispatch.assert_called_once_with({"type": "Follow"}, context={"follow": follow}) + + +def test_can_undo_library_follow(factories, logged_in_api_client, mocker): + dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch") + actor = logged_in_api_client.user.create_actor() + follow = factories["federation.LibraryFollow"](actor=actor) + delete = mocker.patch.object(follow.__class__, "delete") + url = reverse( + "api:v1:federation:library-follows-detail", kwargs={"uuid": follow.uuid} + ) + response = logged_in_api_client.delete(url) + + assert response.status_code == 204 + + delete.assert_called_once_with() + dispatch.assert_called_once_with( + {"type": "Undo", "object": {"type": "Follow"}}, context={"follow": follow} + ) + + +@pytest.mark.parametrize("action", ["accept", "reject"]) +def test_user_cannot_edit_someone_else_library_follow( + factories, logged_in_api_client, action +): + logged_in_api_client.user.create_actor() + follow = factories["federation.LibraryFollow"]() + url = reverse( + "api:v1:federation:library-follows-{}".format(action), + kwargs={"uuid": follow.uuid}, + ) + response = logged_in_api_client.post(url) + + assert response.status_code == 404 + + +@pytest.mark.parametrize("action,expected", [("accept", True), ("reject", False)]) +def test_user_can_accept_or_reject_own_follows( + factories, logged_in_api_client, action, expected, mocker +): + mocked_dispatch = mocker.patch( + "funkwhale_api.federation.activity.OutboxRouter.dispatch" + ) + actor = logged_in_api_client.user.create_actor() + follow = factories["federation.LibraryFollow"](target__actor=actor) + url = reverse( + "api:v1:federation:library-follows-{}".format(action), + kwargs={"uuid": follow.uuid}, + ) + response = logged_in_api_client.post(url) + + assert response.status_code == 204 + + follow.refresh_from_db() + + assert follow.approved is expected + + mocked_dispatch.assert_called_once_with( + {"type": "Accept"}, context={"follow": follow} + ) + + +def test_user_can_list_inbox_items(factories, logged_in_api_client): + actor = logged_in_api_client.user.create_actor() + ii = factories["federation.InboxItem"]( + activity__type="Follow", actor=actor, type="to" + ) + + factories["federation.InboxItem"](activity__type="Follow", actor=actor, type="cc") + factories["federation.InboxItem"](activity__type="Follow", type="to") + + url = reverse("api:v1:federation:inbox-list") + + response = logged_in_api_client.get(url) + + assert response.status_code == 200 + assert response.data == { + "count": 1, + "results": [api_serializers.InboxItemSerializer(ii).data], + "next": None, + "previous": None, + } + + +def test_user_can_update_read_status_of_inbox_item(factories, logged_in_api_client): + actor = logged_in_api_client.user.create_actor() + ii = factories["federation.InboxItem"]( + activity__type="Follow", actor=actor, type="to" + ) + + url = reverse("api:v1:federation:inbox-detail", kwargs={"pk": ii.pk}) + + response = logged_in_api_client.patch(url, {"is_read": True}) + assert response.status_code == 200 + + ii.refresh_from_db() + + assert ii.is_read is True diff --git a/api/tests/federation/test_authentication.py b/api/tests/federation/test_authentication.py index 95cec5d2a..100971a3b 100644 --- a/api/tests/federation/test_authentication.py +++ b/api/tests/federation/test_authentication.py @@ -11,6 +11,7 @@ def test_authenticate(factories, mocker, api_request): "type": "Person", "outbox": "https://test.com", "inbox": "https://test.com", + "followers": "https://test.com", "preferredUsername": "test", "publicKey": { "publicKeyPem": public.decode("utf-8"), @@ -36,4 +37,4 @@ def test_authenticate(factories, mocker, api_request): assert user.is_anonymous is True assert actor.public_key == public.decode("utf-8") - assert actor.url == actor_url + assert actor.fid == actor_url diff --git a/api/tests/federation/test_library.py b/api/tests/federation/test_library.py deleted file mode 100644 index 4e187e479..000000000 --- a/api/tests/federation/test_library.py +++ /dev/null @@ -1,64 +0,0 @@ -from funkwhale_api.federation import library, serializers - - -def test_library_scan_from_account_name(mocker, factories): - actor = factories["federation.Actor"]( - preferred_username="library", domain="test.library" - ) - get_resource_result = {"actor_url": actor.url} - get_resource = mocker.patch( - "funkwhale_api.federation.webfinger.get_resource", - return_value=get_resource_result, - ) - - actor_data = serializers.ActorSerializer(actor).data - actor_data["manuallyApprovesFollowers"] = False - actor_data["url"] = [ - { - "type": "Link", - "name": "library", - "mediaType": "application/activity+json", - "href": "https://test.library", - } - ] - get_actor_data = mocker.patch( - "funkwhale_api.federation.actors.get_actor_data", return_value=actor_data - ) - - get_library_data_result = {"test": "test"} - get_library_data = mocker.patch( - "funkwhale_api.federation.library.get_library_data", - return_value=get_library_data_result, - ) - - result = library.scan_from_account_name("library@test.actor") - - get_resource.assert_called_once_with("acct:library@test.actor") - get_actor_data.assert_called_once_with(actor.url) - get_library_data.assert_called_once_with(actor_data["url"][0]["href"]) - - assert result == { - "webfinger": get_resource_result, - "actor": actor_data, - "library": get_library_data_result, - "local": {"following": False, "awaiting_approval": False}, - } - - -def test_get_library_data(r_mock, factories): - actor = factories["federation.Actor"]() - url = "https://test.library" - conf = {"id": url, "items": [], "actor": actor, "page_size": 5} - data = serializers.PaginatedCollectionSerializer(conf).data - r_mock.get(url, json=data) - - result = library.get_library_data(url) - for f in ["totalItems", "actor", "id", "type"]: - assert result[f] == data[f] - - -def test_get_library_data_requires_authentication(r_mock, factories): - url = "https://test.library" - r_mock.get(url, status_code=403) - result = library.get_library_data(url) - assert result["errors"] == ["Permission denied while scanning library"] diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py index 61d0aea96..4a6131934 100644 --- a/api/tests/federation/test_models.py +++ b/api/tests/federation/test_models.py @@ -20,12 +20,37 @@ def test_cannot_duplicate_follow(factories): def test_follow_federation_url(factories): follow = factories["federation.Follow"](local=True) - expected = "{}#follows/{}".format(follow.actor.url, follow.uuid) + expected = "{}#follows/{}".format(follow.actor.fid, follow.uuid) - assert follow.get_federation_url() == expected + assert follow.get_federation_id() == expected -def test_library_model_unique_per_actor(factories): - library = factories["federation.Library"]() - with pytest.raises(db.IntegrityError): - factories["federation.Library"](actor=library.actor) +def test_actor_get_quota(factories): + library = factories["music.Library"]() + factories["music.Upload"]( + library=library, + import_status="pending", + audio_file__from_path=None, + audio_file__data=b"a", + ) + factories["music.Upload"]( + library=library, + import_status="skipped", + audio_file__from_path=None, + audio_file__data=b"aa", + ) + factories["music.Upload"]( + library=library, + import_status="errored", + audio_file__from_path=None, + audio_file__data=b"aaa", + ) + factories["music.Upload"]( + library=library, + import_status="finished", + audio_file__from_path=None, + audio_file__data=b"aaaa", + ) + expected = {"total": 10, "pending": 1, "skipped": 2, "errored": 3, "finished": 4} + + assert library.actor.get_current_usage() == expected diff --git a/api/tests/federation/test_permissions.py b/api/tests/federation/test_permissions.py deleted file mode 100644 index 75f76077c..000000000 --- a/api/tests/federation/test_permissions.py +++ /dev/null @@ -1,61 +0,0 @@ -from rest_framework.views import APIView - -from funkwhale_api.federation import actors, permissions - - -def test_library_follower(factories, api_request, anonymous_user, preferences): - preferences["federation__music_needs_approval"] = True - view = APIView.as_view() - permission = permissions.LibraryFollower() - request = api_request.get("/") - setattr(request, "user", anonymous_user) - check = permission.has_permission(request, view) - - assert check is False - - -def test_library_follower_actor_non_follower( - factories, api_request, anonymous_user, preferences -): - preferences["federation__music_needs_approval"] = True - actor = factories["federation.Actor"]() - view = APIView.as_view() - permission = permissions.LibraryFollower() - request = api_request.get("/") - setattr(request, "user", anonymous_user) - setattr(request, "actor", actor) - check = permission.has_permission(request, view) - - assert check is False - - -def test_library_follower_actor_follower_not_approved( - factories, api_request, anonymous_user, preferences -): - preferences["federation__music_needs_approval"] = True - library = actors.SYSTEM_ACTORS["library"].get_actor_instance() - follow = factories["federation.Follow"](target=library, approved=False) - view = APIView.as_view() - permission = permissions.LibraryFollower() - request = api_request.get("/") - setattr(request, "user", anonymous_user) - setattr(request, "actor", follow.actor) - check = permission.has_permission(request, view) - - assert check is False - - -def test_library_follower_actor_follower( - factories, api_request, anonymous_user, preferences -): - preferences["federation__music_needs_approval"] = True - library = actors.SYSTEM_ACTORS["library"].get_actor_instance() - follow = factories["federation.Follow"](target=library, approved=True) - view = APIView.as_view() - permission = permissions.LibraryFollower() - request = api_request.get("/") - setattr(request, "user", anonymous_user) - setattr(request, "actor", follow.actor) - check = permission.has_permission(request, view) - - assert check is True diff --git a/api/tests/federation/test_routes.py b/api/tests/federation/test_routes.py new file mode 100644 index 000000000..79d194f56 --- /dev/null +++ b/api/tests/federation/test_routes.py @@ -0,0 +1,355 @@ +import pytest + +from funkwhale_api.federation import routes, serializers + + +@pytest.mark.parametrize( + "route,handler", + [ + ({"type": "Follow"}, routes.inbox_follow), + ({"type": "Accept"}, routes.inbox_accept), + ({"type": "Create", "object.type": "Audio"}, routes.inbox_create_audio), + ({"type": "Delete", "object.type": "Library"}, routes.inbox_delete_library), + ({"type": "Delete", "object.type": "Audio"}, routes.inbox_delete_audio), + ({"type": "Undo", "object.type": "Follow"}, routes.inbox_undo_follow), + ], +) +def test_inbox_routes(route, handler): + for r, h in routes.inbox.routes: + if r == route: + assert h == handler + return + + assert False, "Inbox route {} not found".format(route) + + +@pytest.mark.parametrize( + "route,handler", + [ + ({"type": "Accept"}, routes.outbox_accept), + ({"type": "Follow"}, routes.outbox_follow), + ({"type": "Create", "object.type": "Audio"}, routes.outbox_create_audio), + ({"type": "Delete", "object.type": "Library"}, routes.outbox_delete_library), + ({"type": "Delete", "object.type": "Audio"}, routes.outbox_delete_audio), + ({"type": "Undo", "object.type": "Follow"}, routes.outbox_undo_follow), + ], +) +def test_outbox_routes(route, handler): + for r, h in routes.outbox.routes: + if r == route: + assert h == handler + return + + assert False, "Outbox route {} not found".format(route) + + +def test_inbox_follow_library_autoapprove(factories, mocker): + mocked_outbox_dispatch = mocker.patch( + "funkwhale_api.federation.activity.OutboxRouter.dispatch" + ) + + local_actor = factories["users.User"]().create_actor() + remote_actor = factories["federation.Actor"]() + library = factories["music.Library"](actor=local_actor, privacy_level="everyone") + ii = factories["federation.InboxItem"](actor=local_actor) + + payload = { + "type": "Follow", + "id": "https://test.follow", + "actor": remote_actor.fid, + "object": library.fid, + } + + result = routes.inbox_follow( + payload, + context={"actor": remote_actor, "inbox_items": [ii], "raise_exception": True}, + ) + follow = library.received_follows.latest("id") + + assert result["object"] == library + assert result["related_object"] == follow + + assert follow.fid == payload["id"] + assert follow.actor == remote_actor + assert follow.approved is True + + mocked_outbox_dispatch.assert_called_once_with( + {"type": "Accept"}, context={"follow": follow} + ) + + +def test_inbox_follow_library_manual_approve(factories, mocker): + mocked_outbox_dispatch = mocker.patch( + "funkwhale_api.federation.activity.OutboxRouter.dispatch" + ) + + local_actor = factories["users.User"]().create_actor() + remote_actor = factories["federation.Actor"]() + library = factories["music.Library"](actor=local_actor, privacy_level="me") + ii = factories["federation.InboxItem"](actor=local_actor) + + payload = { + "type": "Follow", + "id": "https://test.follow", + "actor": remote_actor.fid, + "object": library.fid, + } + + result = routes.inbox_follow( + payload, + context={"actor": remote_actor, "inbox_items": [ii], "raise_exception": True}, + ) + follow = library.received_follows.latest("id") + + assert result["object"] == library + assert result["related_object"] == follow + + assert follow.fid == payload["id"] + assert follow.actor == remote_actor + assert follow.approved is None + + mocked_outbox_dispatch.assert_not_called() + + +def test_outbox_accept(factories, mocker): + remote_actor = factories["federation.Actor"]() + follow = factories["federation.LibraryFollow"](actor=remote_actor) + + activity = list(routes.outbox_accept({"follow": follow}))[0] + + serializer = serializers.AcceptFollowSerializer( + follow, context={"actor": follow.target.actor} + ) + expected = serializer.data + expected["to"] = [follow.actor] + + assert activity["payload"] == expected + assert activity["actor"] == follow.target.actor + assert activity["object"] == follow + + +def test_inbox_accept(factories, mocker): + mocked_scan = mocker.patch("funkwhale_api.music.models.Library.schedule_scan") + local_actor = factories["users.User"]().create_actor() + remote_actor = factories["federation.Actor"]() + follow = factories["federation.LibraryFollow"]( + actor=local_actor, target__actor=remote_actor + ) + assert follow.approved is None + serializer = serializers.AcceptFollowSerializer( + follow, context={"actor": remote_actor} + ) + ii = factories["federation.InboxItem"](actor=local_actor) + result = routes.inbox_accept( + serializer.data, + context={"actor": remote_actor, "inbox_items": [ii], "raise_exception": True}, + ) + assert result["object"] == follow + assert result["related_object"] == follow.target + + follow.refresh_from_db() + + assert follow.approved is True + mocked_scan.assert_called_once_with(actor=follow.actor) + + +def test_outbox_follow_library(factories, mocker): + follow = factories["federation.LibraryFollow"]() + activity = list(routes.outbox_follow({"follow": follow}))[0] + serializer = serializers.FollowSerializer(follow, context={"actor": follow.actor}) + expected = serializer.data + expected["to"] = [follow.target.actor] + + assert activity["payload"] == expected + assert activity["actor"] == follow.actor + assert activity["object"] == follow.target + + +def test_outbox_create_audio(factories, mocker): + upload = factories["music.Upload"]() + activity = list(routes.outbox_create_audio({"upload": upload}))[0] + serializer = serializers.ActivitySerializer( + { + "type": "Create", + "object": serializers.UploadSerializer(upload).data, + "actor": upload.library.actor.fid, + } + ) + expected = serializer.data + expected["to"] = [{"type": "followers", "target": upload.library}] + + assert dict(activity["payload"]) == dict(expected) + assert activity["actor"] == upload.library.actor + assert activity["target"] == upload.library + assert activity["object"] == upload + + +def test_inbox_create_audio(factories, mocker): + activity = factories["federation.Activity"]() + upload = factories["music.Upload"](bitrate=42, duration=55) + payload = { + "type": "Create", + "actor": upload.library.actor.fid, + "object": serializers.UploadSerializer(upload).data, + } + library = upload.library + upload.delete() + init = mocker.spy(serializers.UploadSerializer, "__init__") + save = mocker.spy(serializers.UploadSerializer, "save") + assert library.uploads.count() == 0 + result = routes.inbox_create_audio( + payload, + context={"actor": library.actor, "raise_exception": True, "activity": activity}, + ) + assert library.uploads.count() == 1 + assert result == {"object": library.uploads.latest("id"), "target": library} + + assert init.call_count == 1 + args = init.call_args + assert args[1]["data"] == payload["object"] + assert args[1]["context"] == {"activity": activity, "actor": library.actor} + assert save.call_count == 1 + + +def test_inbox_delete_library(factories): + activity = factories["federation.Activity"]() + + library = factories["music.Library"]() + payload = { + "type": "Delete", + "actor": library.actor.fid, + "object": {"type": "Library", "id": library.fid}, + } + + routes.inbox_delete_library( + payload, + context={"actor": library.actor, "raise_exception": True, "activity": activity}, + ) + + with pytest.raises(library.__class__.DoesNotExist): + library.refresh_from_db() + + +def test_inbox_delete_library_impostor(factories): + activity = factories["federation.Activity"]() + impostor = factories["federation.Actor"]() + library = factories["music.Library"]() + payload = { + "type": "Delete", + "actor": library.actor.fid, + "object": {"type": "Library", "id": library.fid}, + } + + routes.inbox_delete_library( + payload, + context={"actor": impostor, "raise_exception": True, "activity": activity}, + ) + + # not deleted, should still be here + library.refresh_from_db() + + +def test_outbox_delete_library(factories): + library = factories["music.Library"]() + activity = list(routes.outbox_delete_library({"library": library}))[0] + expected = serializers.ActivitySerializer( + {"type": "Delete", "object": {"type": "Library", "id": library.fid}} + ).data + + expected["to"] = [{"type": "followers", "target": library}] + + assert dict(activity["payload"]) == dict(expected) + assert activity["actor"] == library.actor + + +def test_inbox_delete_audio(factories): + activity = factories["federation.Activity"]() + + upload = factories["music.Upload"]() + library = upload.library + payload = { + "type": "Delete", + "actor": library.actor.fid, + "object": {"type": "Audio", "id": [upload.fid]}, + } + + routes.inbox_delete_audio( + payload, + context={"actor": library.actor, "raise_exception": True, "activity": activity}, + ) + + with pytest.raises(upload.__class__.DoesNotExist): + upload.refresh_from_db() + + +def test_inbox_delete_audio_impostor(factories): + activity = factories["federation.Activity"]() + impostor = factories["federation.Actor"]() + upload = factories["music.Upload"]() + library = upload.library + payload = { + "type": "Delete", + "actor": library.actor.fid, + "object": {"type": "Audio", "id": [upload.fid]}, + } + + routes.inbox_delete_audio( + payload, + context={"actor": impostor, "raise_exception": True, "activity": activity}, + ) + + # not deleted, should still be here + upload.refresh_from_db() + + +def test_outbox_delete_audio(factories): + upload = factories["music.Upload"]() + activity = list(routes.outbox_delete_audio({"uploads": [upload]}))[0] + expected = serializers.ActivitySerializer( + {"type": "Delete", "object": {"type": "Audio", "id": [upload.fid]}} + ).data + + expected["to"] = [{"type": "followers", "target": upload.library}] + + assert dict(activity["payload"]) == dict(expected) + assert activity["actor"] == upload.library.actor + + +def test_inbox_delete_follow_library(factories): + local_actor = factories["users.User"]().create_actor() + remote_actor = factories["federation.Actor"]() + follow = factories["federation.LibraryFollow"]( + actor=local_actor, target__actor=remote_actor, approved=True + ) + assert follow.approved is True + serializer = serializers.UndoFollowSerializer( + follow, context={"actor": local_actor} + ) + ii = factories["federation.InboxItem"](actor=local_actor) + routes.inbox_undo_follow( + serializer.data, + context={"actor": local_actor, "inbox_items": [ii], "raise_exception": True}, + ) + with pytest.raises(follow.__class__.DoesNotExist): + follow.refresh_from_db() + + +def test_outbox_delete_follow_library(factories): + remote_actor = factories["federation.Actor"]() + local_actor = factories["federation.Actor"](local=True) + follow = factories["federation.LibraryFollow"]( + actor=local_actor, target__actor=remote_actor + ) + + activity = list(routes.outbox_undo_follow({"follow": follow}))[0] + + serializer = serializers.UndoFollowSerializer( + follow, context={"actor": follow.actor} + ) + expected = serializer.data + expected["to"] = [follow.target.actor] + + assert activity["payload"] == expected + assert activity["actor"] == follow.actor + assert activity["object"] == follow + assert activity["related_object"] == follow.target diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py index b42c843ee..c43647070 100644 --- a/api/tests/federation/test_serializers.py +++ b/api/tests/federation/test_serializers.py @@ -1,8 +1,11 @@ -import pendulum +import io import pytest -from django.core.paginator import Paginator +import uuid -from funkwhale_api.federation import actors, models, serializers, utils +from django.core.paginator import Paginator +from django.utils import timezone + +from funkwhale_api.federation import models, serializers, utils def test_actor_serializer_from_ap(db): @@ -31,7 +34,7 @@ def test_actor_serializer_from_ap(db): actor = serializer.build() - assert actor.url == payload["id"] + assert actor.fid == payload["id"] assert actor.inbox_url == payload["inbox"] assert actor.outbox_url == payload["outbox"] assert actor.shared_inbox_url == payload["endpoints"]["sharedInbox"] @@ -62,7 +65,7 @@ def test_actor_serializer_only_mandatory_field_from_ap(db): actor = serializer.build() - assert actor.url == payload["id"] + assert actor.fid == payload["id"] assert actor.inbox_url == payload["inbox"] assert actor.outbox_url == payload["outbox"] assert actor.followers_url == payload["followers"] @@ -98,7 +101,7 @@ def test_actor_serializer_to_ap(): "endpoints": {"sharedInbox": "https://test.federation/inbox"}, } ac = models.Actor( - url=expected["id"], + fid=expected["id"], inbox_url=expected["inbox"], outbox_url=expected["outbox"], shared_inbox_url=expected["endpoints"]["sharedInbox"], @@ -130,7 +133,7 @@ def test_webfinger_serializer(): "aliases": ["https://test.federation/federation/instance/actor"], } actor = models.Actor( - url=expected["links"][0]["href"], + fid=expected["links"][0]["href"], preferred_username="service", domain="test.federation", ) @@ -149,10 +152,10 @@ def test_follow_serializer_to_ap(factories): "https://w3id.org/security/v1", {}, ], - "id": follow.get_federation_url(), + "id": follow.get_federation_id(), "type": "Follow", - "actor": follow.actor.url, - "object": follow.target.url, + "actor": follow.actor.fid, + "object": follow.target.fid, } assert serializer.data == expected @@ -165,8 +168,8 @@ def test_follow_serializer_save(factories): data = { "id": "https://test.follow", "type": "Follow", - "actor": actor.url, - "object": target.url, + "actor": actor.fid, + "object": target.fid, } serializer = serializers.FollowSerializer(data=data) @@ -188,8 +191,8 @@ def test_follow_serializer_save_validates_on_context(factories): data = { "id": "https://test.follow", "type": "Follow", - "actor": actor.url, - "object": target.url, + "actor": actor.fid, + "object": target.fid, } serializer = serializers.FollowSerializer( data=data, context={"follow_actor": impostor, "follow_target": impostor} @@ -210,9 +213,9 @@ def test_accept_follow_serializer_representation(factories): "https://w3id.org/security/v1", {}, ], - "id": follow.get_federation_url() + "/accept", + "id": follow.get_federation_id() + "/accept", "type": "Accept", - "actor": follow.target.url, + "actor": follow.target.fid, "object": serializers.FollowSerializer(follow).data, } @@ -230,9 +233,9 @@ def test_accept_follow_serializer_save(factories): "https://w3id.org/security/v1", {}, ], - "id": follow.get_federation_url() + "/accept", + "id": follow.get_federation_id() + "/accept", "type": "Accept", - "actor": follow.target.url, + "actor": follow.target.fid, "object": serializers.FollowSerializer(follow).data, } @@ -254,7 +257,7 @@ def test_accept_follow_serializer_validates_on_context(factories): "https://w3id.org/security/v1", {}, ], - "id": follow.get_federation_url() + "/accept", + "id": follow.get_federation_id() + "/accept", "type": "Accept", "actor": impostor.url, "object": serializers.FollowSerializer(follow).data, @@ -278,9 +281,9 @@ def test_undo_follow_serializer_representation(factories): "https://w3id.org/security/v1", {}, ], - "id": follow.get_federation_url() + "/undo", + "id": follow.get_federation_id() + "/undo", "type": "Undo", - "actor": follow.actor.url, + "actor": follow.actor.fid, "object": serializers.FollowSerializer(follow).data, } @@ -298,9 +301,9 @@ def test_undo_follow_serializer_save(factories): "https://w3id.org/security/v1", {}, ], - "id": follow.get_federation_url() + "/undo", + "id": follow.get_federation_id() + "/undo", "type": "Undo", - "actor": follow.actor.url, + "actor": follow.actor.fid, "object": serializers.FollowSerializer(follow).data, } @@ -321,7 +324,7 @@ def test_undo_follow_serializer_validates_on_context(factories): "https://w3id.org/security/v1", {}, ], - "id": follow.get_federation_url() + "/undo", + "id": follow.get_federation_id() + "/undo", "type": "Undo", "actor": impostor.url, "object": serializers.FollowSerializer(follow).data, @@ -337,13 +340,13 @@ def test_undo_follow_serializer_validates_on_context(factories): def test_paginated_collection_serializer(factories): - tfs = factories["music.TrackFile"].create_batch(size=5) + uploads = factories["music.Upload"].create_batch(size=5) actor = factories["federation.Actor"](local=True) conf = { "id": "https://test.federation/test", - "items": tfs, - "item_serializer": serializers.AudioSerializer, + "items": uploads, + "item_serializer": serializers.UploadSerializer, "actor": actor, "page_size": 2, } @@ -355,8 +358,8 @@ def test_paginated_collection_serializer(factories): ], "type": "Collection", "id": conf["id"], - "actor": actor.url, - "totalItems": len(tfs), + "actor": actor.fid, + "totalItems": len(uploads), "current": conf["id"] + "?page=1", "last": conf["id"] + "?page=3", "first": conf["id"] + "?page=1", @@ -426,7 +429,7 @@ def test_collection_page_serializer_can_validate_child(): } serializer = serializers.CollectionPageSerializer( - data=data, context={"item_serializer": serializers.AudioSerializer} + data=data, context={"item_serializer": serializers.UploadSerializer} ) # child are validated but not included in data if not valid @@ -435,14 +438,14 @@ def test_collection_page_serializer_can_validate_child(): def test_collection_page_serializer(factories): - tfs = factories["music.TrackFile"].create_batch(size=5) + uploads = factories["music.Upload"].create_batch(size=5) actor = factories["federation.Actor"](local=True) conf = { "id": "https://test.federation/test", - "item_serializer": serializers.AudioSerializer, + "item_serializer": serializers.UploadSerializer, "actor": actor, - "page": Paginator(tfs, 2).page(2), + "page": Paginator(uploads, 2).page(2), } expected = { "@context": [ @@ -452,8 +455,8 @@ def test_collection_page_serializer(factories): ], "type": "CollectionPage", "id": conf["id"] + "?page=2", - "actor": actor.url, - "totalItems": len(tfs), + "actor": actor.fid, + "totalItems": len(uploads), "partOf": conf["id"], "prev": conf["id"] + "?page=1", "next": conf["id"] + "?page=3", @@ -472,217 +475,388 @@ def test_collection_page_serializer(factories): assert serializer.data == expected -def test_activity_pub_audio_serializer_to_library_track(factories): - remote_library = factories["federation.Library"]() - audio = factories["federation.Audio"]() - serializer = serializers.AudioSerializer( - data=audio, context={"library": remote_library} +def test_music_library_serializer_to_ap(factories): + library = factories["music.Library"]() + # pending, errored and skippednot included + factories["music.Upload"](import_status="pending") + factories["music.Upload"](import_status="errored") + factories["music.Upload"](import_status="finished") + serializer = serializers.LibrarySerializer(library) + expected = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {}, + ], + "type": "Library", + "id": library.fid, + "name": library.name, + "summary": library.description, + "audience": "", + "actor": library.actor.fid, + "totalItems": 0, + "current": library.fid + "?page=1", + "last": library.fid + "?page=1", + "first": library.fid + "?page=1", + "followers": library.followers_url, + } + + assert serializer.data == expected + + +def test_music_library_serializer_from_public(factories, mocker): + actor = factories["federation.Actor"]() + retrieve = mocker.patch( + "funkwhale_api.federation.utils.retrieve", return_value=actor ) + data = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {}, + ], + "audience": "https://www.w3.org/ns/activitystreams#Public", + "name": "Hello", + "summary": "World", + "type": "Library", + "id": "https://library.id", + "followers": "https://library.id/followers", + "actor": actor.fid, + "totalItems": 12, + "first": "https://library.id?page=1", + "last": "https://library.id?page=2", + } + serializer = serializers.LibrarySerializer(data=data) assert serializer.is_valid(raise_exception=True) - lt = serializer.save() + library = serializer.save() - assert lt.pk is not None - assert lt.url == audio["id"] - assert lt.library == remote_library - assert lt.audio_url == audio["url"]["href"] - assert lt.audio_mimetype == audio["url"]["mediaType"] - assert lt.metadata == audio["metadata"] - assert lt.title == audio["metadata"]["recording"]["title"] - assert lt.artist_name == audio["metadata"]["artist"]["name"] - assert lt.album_title == audio["metadata"]["release"]["title"] - assert lt.published_date == pendulum.parse(audio["published"]) + assert library.actor == actor + assert library.fid == data["id"] + assert library.uploads_count == data["totalItems"] + assert library.privacy_level == "everyone" + assert library.name == "Hello" + assert library.description == "World" + assert library.followers_url == data["followers"] - -def test_activity_pub_audio_serializer_to_library_track_no_duplicate(factories): - remote_library = factories["federation.Library"]() - audio = factories["federation.Audio"]() - serializer1 = serializers.AudioSerializer( - data=audio, context={"library": remote_library} - ) - serializer2 = serializers.AudioSerializer( - data=audio, context={"library": remote_library} + retrieve.assert_called_once_with( + actor.fid, + queryset=actor.__class__, + serializer_class=serializers.ActorSerializer, ) - assert serializer1.is_valid() is True - assert serializer2.is_valid() is True - lt1 = serializer1.save() - lt2 = serializer2.save() +def test_music_library_serializer_from_private(factories, mocker): + actor = factories["federation.Actor"]() + retrieve = mocker.patch( + "funkwhale_api.federation.utils.retrieve", return_value=actor + ) + data = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {}, + ], + "audience": "", + "name": "Hello", + "summary": "World", + "type": "Library", + "id": "https://library.id", + "followers": "https://library.id/followers", + "actor": actor.fid, + "totalItems": 12, + "first": "https://library.id?page=1", + "last": "https://library.id?page=2", + } + serializer = serializers.LibrarySerializer(data=data) - assert lt1 == lt2 - assert models.LibraryTrack.objects.count() == 1 + assert serializer.is_valid(raise_exception=True) + + library = serializer.save() + + assert library.actor == actor + assert library.fid == data["id"] + assert library.uploads_count == data["totalItems"] + assert library.privacy_level == "me" + assert library.name == "Hello" + assert library.description == "World" + assert library.followers_url == data["followers"] + retrieve.assert_called_once_with( + actor.fid, + queryset=actor.__class__, + serializer_class=serializers.ActorSerializer, + ) + + +def test_activity_pub_artist_serializer_to_ap(factories): + artist = factories["music.Artist"]() + expected = { + "@context": serializers.AP_CONTEXT, + "type": "Artist", + "id": artist.fid, + "name": artist.name, + "musicbrainzId": artist.mbid, + "published": artist.creation_date.isoformat(), + } + serializer = serializers.ArtistSerializer(artist) + + assert serializer.data == expected + + +def test_activity_pub_album_serializer_to_ap(factories): + album = factories["music.Album"]() + + expected = { + "@context": serializers.AP_CONTEXT, + "type": "Album", + "id": album.fid, + "name": album.title, + "cover": { + "type": "Link", + "mediaType": "image/jpeg", + "href": utils.full_url(album.cover.url), + }, + "musicbrainzId": album.mbid, + "published": album.creation_date.isoformat(), + "released": album.release_date.isoformat(), + "artists": [ + serializers.ArtistSerializer( + album.artist, context={"include_ap_context": False} + ).data + ], + } + serializer = serializers.AlbumSerializer(album) + + assert serializer.data == expected + + +def test_activity_pub_track_serializer_to_ap(factories): + track = factories["music.Track"]() + expected = { + "@context": serializers.AP_CONTEXT, + "published": track.creation_date.isoformat(), + "type": "Track", + "musicbrainzId": track.mbid, + "id": track.fid, + "name": track.title, + "position": track.position, + "artists": [ + serializers.ArtistSerializer( + track.artist, context={"include_ap_context": False} + ).data + ], + "album": serializers.AlbumSerializer( + track.album, context={"include_ap_context": False} + ).data, + } + serializer = serializers.TrackSerializer(track) + + assert serializer.data == expected + + +def test_activity_pub_track_serializer_from_ap(factories, r_mock): + activity = factories["federation.Activity"]() + published = timezone.now() + released = timezone.now().date() + data = { + "type": "Track", + "id": "http://hello.track", + "published": published.isoformat(), + "musicbrainzId": str(uuid.uuid4()), + "name": "Black in back", + "position": 5, + "album": { + "type": "Album", + "id": "http://hello.album", + "name": "Purple album", + "musicbrainzId": str(uuid.uuid4()), + "published": published.isoformat(), + "released": released.isoformat(), + "cover": { + "type": "Link", + "href": "https://cover.image/test.png", + "mediaType": "image/png", + }, + "artists": [ + { + "type": "Artist", + "id": "http://hello.artist", + "name": "John Smith", + "musicbrainzId": str(uuid.uuid4()), + "published": published.isoformat(), + } + ], + }, + "artists": [ + { + "type": "Artist", + "id": "http://hello.trackartist", + "name": "Bob Smith", + "musicbrainzId": str(uuid.uuid4()), + "published": published.isoformat(), + } + ], + } + r_mock.get(data["album"]["cover"]["href"], body=io.BytesIO(b"coucou")) + serializer = serializers.TrackSerializer(data=data, context={"activity": activity}) + assert serializer.is_valid(raise_exception=True) + + track = serializer.save() + album = track.album + artist = track.artist + album_artist = track.album.artist + + assert track.from_activity == activity + assert track.fid == data["id"] + assert track.title == data["name"] + assert track.position == data["position"] + assert track.creation_date == published + assert str(track.mbid) == data["musicbrainzId"] + + assert album.from_activity == activity + assert album.cover.read() == b"coucou" + assert album.cover.path.endswith(".png") + assert album.title == data["album"]["name"] + assert album.fid == data["album"]["id"] + assert str(album.mbid) == data["album"]["musicbrainzId"] + assert album.creation_date == published + assert album.release_date == released + + assert artist.from_activity == activity + assert artist.name == data["artists"][0]["name"] + assert artist.fid == data["artists"][0]["id"] + assert str(artist.mbid) == data["artists"][0]["musicbrainzId"] + assert artist.creation_date == published + + assert album_artist.from_activity == activity + assert album_artist.name == data["album"]["artists"][0]["name"] + assert album_artist.fid == data["album"]["artists"][0]["id"] + assert str(album_artist.mbid) == data["album"]["artists"][0]["musicbrainzId"] + assert album_artist.creation_date == published + + +def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock): + activity = factories["federation.Activity"]() + library = factories["music.Library"]() + + published = timezone.now() + updated = timezone.now() + released = timezone.now().date() + data = { + "@context": serializers.AP_CONTEXT, + "type": "Audio", + "id": "https://track.file", + "name": "Ignored", + "published": published.isoformat(), + "updated": updated.isoformat(), + "duration": 43, + "bitrate": 42, + "size": 66, + "url": {"href": "https://audio.file", "type": "Link", "mediaType": "audio/mp3"}, + "library": library.fid, + "track": { + "type": "Track", + "id": "http://hello.track", + "published": published.isoformat(), + "musicbrainzId": str(uuid.uuid4()), + "name": "Black in back", + "position": 5, + "album": { + "type": "Album", + "id": "http://hello.album", + "name": "Purple album", + "musicbrainzId": str(uuid.uuid4()), + "published": published.isoformat(), + "released": released.isoformat(), + "cover": { + "type": "Link", + "href": "https://cover.image/test.png", + "mediaType": "image/png", + }, + "artists": [ + { + "type": "Artist", + "id": "http://hello.artist", + "name": "John Smith", + "musicbrainzId": str(uuid.uuid4()), + "published": published.isoformat(), + } + ], + }, + "artists": [ + { + "type": "Artist", + "id": "http://hello.trackartist", + "name": "Bob Smith", + "musicbrainzId": str(uuid.uuid4()), + "published": published.isoformat(), + } + ], + }, + } + r_mock.get(data["track"]["album"]["cover"]["href"], body=io.BytesIO(b"coucou")) + + serializer = serializers.UploadSerializer(data=data, context={"activity": activity}) + assert serializer.is_valid(raise_exception=True) + track_create = mocker.spy(serializers.TrackSerializer, "create") + upload = serializer.save() + + assert upload.track.from_activity == activity + assert upload.from_activity == activity + assert track_create.call_count == 1 + assert upload.fid == data["id"] + assert upload.track.fid == data["track"]["id"] + assert upload.duration == data["duration"] + assert upload.size == data["size"] + assert upload.bitrate == data["bitrate"] + assert upload.source == data["url"]["href"] + assert upload.mimetype == data["url"]["mediaType"] + assert upload.creation_date == published + assert upload.import_status == "finished" + assert upload.modification_date == updated + + +def test_activity_pub_upload_serializer_validtes_library_actor(factories, mocker): + library = factories["music.Library"]() + usurpator = factories["federation.Actor"]() + + serializer = serializers.UploadSerializer(data={}, context={"actor": usurpator}) + + with pytest.raises(serializers.serializers.ValidationError): + serializer.validate_library(library.fid) def test_activity_pub_audio_serializer_to_ap(factories): - tf = factories["music.TrackFile"]( + upload = factories["music.Upload"]( mimetype="audio/mp3", bitrate=42, duration=43, size=44 ) - library = actors.SYSTEM_ACTORS["library"].get_actor_instance() expected = { "@context": serializers.AP_CONTEXT, "type": "Audio", - "id": tf.get_federation_url(), - "name": tf.track.full_name, - "published": tf.creation_date.isoformat(), - "updated": tf.modification_date.isoformat(), - "metadata": { - "artist": { - "musicbrainz_id": tf.track.artist.mbid, - "name": tf.track.artist.name, - }, - "release": { - "musicbrainz_id": tf.track.album.mbid, - "title": tf.track.album.title, - }, - "recording": {"musicbrainz_id": tf.track.mbid, "title": tf.track.title}, - "size": tf.size, - "length": tf.duration, - "bitrate": tf.bitrate, - }, + "id": upload.fid, + "name": upload.track.full_name, + "published": upload.creation_date.isoformat(), + "updated": upload.modification_date.isoformat(), + "duration": upload.duration, + "bitrate": upload.bitrate, + "size": upload.size, "url": { - "href": utils.full_url(tf.path), + "href": utils.full_url(upload.listen_url), "type": "Link", "mediaType": "audio/mp3", }, - "attributedTo": [library.url], + "library": upload.library.fid, + "track": serializers.TrackSerializer( + upload.track, context={"include_ap_context": False} + ).data, } - serializer = serializers.AudioSerializer(tf, context={"actor": library}) + serializer = serializers.UploadSerializer(upload) assert serializer.data == expected -def test_activity_pub_audio_serializer_to_ap_no_mbid(factories): - tf = factories["music.TrackFile"]( - mimetype="audio/mp3", - track__mbid=None, - track__album__mbid=None, - track__album__artist__mbid=None, - ) - library = actors.SYSTEM_ACTORS["library"].get_actor_instance() - expected = { - "@context": serializers.AP_CONTEXT, - "type": "Audio", - "id": tf.get_federation_url(), - "name": tf.track.full_name, - "published": tf.creation_date.isoformat(), - "updated": tf.modification_date.isoformat(), - "metadata": { - "artist": {"name": tf.track.artist.name, "musicbrainz_id": None}, - "release": {"title": tf.track.album.title, "musicbrainz_id": None}, - "recording": {"title": tf.track.title, "musicbrainz_id": None}, - "size": None, - "length": None, - "bitrate": None, - }, - "url": { - "href": utils.full_url(tf.path), - "type": "Link", - "mediaType": "audio/mp3", - }, - "attributedTo": [library.url], - } - - serializer = serializers.AudioSerializer(tf, context={"actor": library}) - - assert serializer.data == expected - - -def test_collection_serializer_to_ap(factories): - tf1 = factories["music.TrackFile"](mimetype="audio/mp3") - tf2 = factories["music.TrackFile"](mimetype="audio/ogg") - library = actors.SYSTEM_ACTORS["library"].get_actor_instance() - expected = { - "@context": serializers.AP_CONTEXT, - "id": "https://test.id", - "actor": library.url, - "totalItems": 2, - "type": "Collection", - "items": [ - serializers.AudioSerializer( - tf1, context={"actor": library, "include_ap_context": False} - ).data, - serializers.AudioSerializer( - tf2, context={"actor": library, "include_ap_context": False} - ).data, - ], - } - - collection = { - "id": expected["id"], - "actor": library, - "items": [tf1, tf2], - "item_serializer": serializers.AudioSerializer, - } - serializer = serializers.CollectionSerializer( - collection, context={"actor": library, "id": "https://test.id"} - ) - - assert serializer.data == expected - - -def test_api_library_create_serializer_save(factories, r_mock): - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - actor = factories["federation.Actor"]() - follow = factories["federation.Follow"](target=actor, actor=library_actor) - actor_data = serializers.ActorSerializer(actor).data - actor_data["url"] = [ - {"href": "https://test.library", "name": "library", "type": "Link"} - ] - library_conf = { - "id": "https://test.library", - "items": range(10), - "actor": actor, - "page_size": 5, - } - library_data = serializers.PaginatedCollectionSerializer(library_conf).data - r_mock.get(actor.url, json=actor_data) - r_mock.get("https://test.library", json=library_data) - data = { - "actor": actor.url, - "autoimport": False, - "federation_enabled": True, - "download_files": False, - } - - serializer = serializers.APILibraryCreateSerializer(data=data) - assert serializer.is_valid(raise_exception=True) is True - library = serializer.save() - follow = models.Follow.objects.get(target=actor, actor=library_actor, approved=None) - - assert library.autoimport is data["autoimport"] - assert library.federation_enabled is data["federation_enabled"] - assert library.download_files is data["download_files"] - assert library.tracks_count == 10 - assert library.actor == actor - assert library.follow == follow - - -def test_tapi_library_track_serializer_not_imported(factories): - lt = factories["federation.LibraryTrack"]() - serializer = serializers.APILibraryTrackSerializer(lt) - - assert serializer.get_status(lt) == "not_imported" - - -def test_tapi_library_track_serializer_imported(factories): - tf = factories["music.TrackFile"](federation=True) - lt = tf.library_track - serializer = serializers.APILibraryTrackSerializer(lt) - - assert serializer.get_status(lt) == "imported" - - -def test_tapi_library_track_serializer_import_pending(factories): - job = factories["music.ImportJob"](federation=True, status="pending") - lt = job.library_track - serializer = serializers.APILibraryTrackSerializer(lt) - - assert serializer.get_status(lt) == "import_pending" - - def test_local_actor_serializer_to_ap(factories): expected = { "@context": [ @@ -708,7 +882,7 @@ def test_local_actor_serializer_to_ap(factories): "endpoints": {"sharedInbox": "https://test.federation/inbox"}, } ac = models.Actor.objects.create( - url=expected["id"], + fid=expected["id"], inbox_url=expected["inbox"], outbox_url=expected["outbox"], shared_inbox_url=expected["endpoints"]["sharedInbox"], @@ -734,3 +908,16 @@ def test_local_actor_serializer_to_ap(factories): serializer = serializers.ActorSerializer(ac) assert serializer.data == expected + + +def test_activity_serializer_validate_recipients_empty(db): + s = serializers.BaseActivitySerializer() + + with pytest.raises(serializers.serializers.ValidationError): + s.validate_recipients({}) + + with pytest.raises(serializers.serializers.ValidationError): + s.validate_recipients({"to": []}) + + with pytest.raises(serializers.serializers.ValidationError): + s.validate_recipients({"cc": []}) diff --git a/api/tests/federation/test_tasks.py b/api/tests/federation/test_tasks.py index bc10eae95..1f58055a2 100644 --- a/api/tests/federation/test_tasks.py +++ b/api/tests/federation/test_tasks.py @@ -1,155 +1,140 @@ import datetime import os import pathlib +import pytest -from django.core.paginator import Paginator from django.utils import timezone -from funkwhale_api.federation import serializers, tasks - - -def test_scan_library_does_nothing_if_federation_disabled(mocker, factories): - library = factories["federation.Library"](federation_enabled=False) - tasks.scan_library(library_id=library.pk) - - assert library.tracks.count() == 0 - - -def test_scan_library_page_does_nothing_if_federation_disabled(mocker, factories): - library = factories["federation.Library"](federation_enabled=False) - tasks.scan_library_page(library_id=library.pk, page_url=None) - - assert library.tracks.count() == 0 - - -def test_scan_library_fetches_page_and_calls_scan_page(mocker, factories, r_mock): - now = timezone.now() - library = factories["federation.Library"](federation_enabled=True) - collection_conf = { - "actor": library.actor, - "id": library.url, - "page_size": 10, - "items": range(10), - } - collection = serializers.PaginatedCollectionSerializer(collection_conf) - scan_page = mocker.patch("funkwhale_api.federation.tasks.scan_library_page.delay") - r_mock.get(collection_conf["id"], json=collection.data) - tasks.scan_library(library_id=library.pk) - - scan_page.assert_called_once_with( - library_id=library.id, page_url=collection.data["first"], until=None - ) - library.refresh_from_db() - assert library.fetched_date > now - - -def test_scan_page_fetches_page_and_creates_tracks(mocker, factories, r_mock): - library = factories["federation.Library"](federation_enabled=True) - tfs = factories["music.TrackFile"].create_batch(size=5) - page_conf = { - "actor": library.actor, - "id": library.url, - "page": Paginator(tfs, 5).page(1), - "item_serializer": serializers.AudioSerializer, - } - page = serializers.CollectionPageSerializer(page_conf) - r_mock.get(page.data["id"], json=page.data) - - tasks.scan_library_page(library_id=library.pk, page_url=page.data["id"]) - - lts = list(library.tracks.all().order_by("-published_date")) - assert len(lts) == 5 - - -def test_scan_page_trigger_next_page_scan_skip_if_same(mocker, factories, r_mock): - patched_scan = mocker.patch( - "funkwhale_api.federation.tasks.scan_library_page.delay" - ) - library = factories["federation.Library"](federation_enabled=True) - tfs = factories["music.TrackFile"].create_batch(size=1) - page_conf = { - "actor": library.actor, - "id": library.url, - "page": Paginator(tfs, 3).page(1), - "item_serializer": serializers.AudioSerializer, - } - page = serializers.CollectionPageSerializer(page_conf) - data = page.data - data["next"] = data["id"] - r_mock.get(page.data["id"], json=data) - - tasks.scan_library_page(library_id=library.pk, page_url=data["id"]) - patched_scan.assert_not_called() - - -def test_scan_page_stops_once_until_is_reached(mocker, factories, r_mock): - library = factories["federation.Library"](federation_enabled=True) - tfs = list(reversed(factories["music.TrackFile"].create_batch(size=5))) - page_conf = { - "actor": library.actor, - "id": library.url, - "page": Paginator(tfs, 3).page(1), - "item_serializer": serializers.AudioSerializer, - } - page = serializers.CollectionPageSerializer(page_conf) - r_mock.get(page.data["id"], json=page.data) - - tasks.scan_library_page( - library_id=library.pk, page_url=page.data["id"], until=tfs[1].creation_date - ) - - lts = list(library.tracks.all().order_by("-published_date")) - assert len(lts) == 2 - for i, tf in enumerate(tfs[:1]): - assert tf.creation_date == lts[i].published_date +from funkwhale_api.federation import tasks def test_clean_federation_music_cache_if_no_listen(preferences, factories): preferences["federation__music_cache_duration"] = 60 - lt1 = factories["federation.LibraryTrack"](with_audio_file=True) - lt2 = factories["federation.LibraryTrack"](with_audio_file=True) - lt3 = factories["federation.LibraryTrack"](with_audio_file=True) - factories["music.TrackFile"](accessed_date=timezone.now(), library_track=lt1) - factories["music.TrackFile"]( - accessed_date=timezone.now() - datetime.timedelta(minutes=61), library_track=lt2 + remote_library = factories["music.Library"]() + upload1 = factories["music.Upload"]( + library=remote_library, accessed_date=timezone.now() ) - factories["music.TrackFile"](accessed_date=None, library_track=lt3) - path1 = lt1.audio_file.path - path2 = lt2.audio_file.path - path3 = lt3.audio_file.path + upload2 = factories["music.Upload"]( + library=remote_library, + accessed_date=timezone.now() - datetime.timedelta(minutes=61), + ) + upload3 = factories["music.Upload"](library=remote_library, accessed_date=None) + # local upload, should not be cleaned + upload4 = factories["music.Upload"](library__actor__local=True, accessed_date=None) + + path1 = upload1.audio_file.path + path2 = upload2.audio_file.path + path3 = upload3.audio_file.path + path4 = upload4.audio_file.path tasks.clean_music_cache() - lt1.refresh_from_db() - lt2.refresh_from_db() - lt3.refresh_from_db() + upload1.refresh_from_db() + upload2.refresh_from_db() + upload3.refresh_from_db() + upload4.refresh_from_db() - assert bool(lt1.audio_file) is True - assert bool(lt2.audio_file) is False - assert bool(lt3.audio_file) is False + assert bool(upload1.audio_file) is True + assert bool(upload2.audio_file) is False + assert bool(upload3.audio_file) is False + assert bool(upload4.audio_file) is True assert os.path.exists(path1) is True assert os.path.exists(path2) is False assert os.path.exists(path3) is False + assert os.path.exists(path4) is True def test_clean_federation_music_cache_orphaned(settings, preferences, factories): preferences["federation__music_cache_duration"] = 60 - path = os.path.join(settings.MEDIA_ROOT, "federation_cache") + path = os.path.join(settings.MEDIA_ROOT, "federation_cache", "tracks") keep_path = os.path.join(os.path.join(path, "1a", "b2"), "keep.ogg") remove_path = os.path.join(os.path.join(path, "c3", "d4"), "remove.ogg") os.makedirs(os.path.dirname(keep_path), exist_ok=True) os.makedirs(os.path.dirname(remove_path), exist_ok=True) pathlib.Path(keep_path).touch() pathlib.Path(remove_path).touch() - lt = factories["federation.LibraryTrack"]( - with_audio_file=True, audio_file__path=keep_path + upload = factories["music.Upload"]( + accessed_date=timezone.now(), audio_file__path=keep_path ) - factories["music.TrackFile"](library_track=lt, accessed_date=timezone.now()) tasks.clean_music_cache() - lt.refresh_from_db() + upload.refresh_from_db() - assert bool(lt.audio_file) is True - assert os.path.exists(lt.audio_file.path) is True + assert bool(upload.audio_file) is True + assert os.path.exists(upload.audio_file.path) is True assert os.path.exists(remove_path) is False + + +def test_handle_in(factories, mocker, now, queryset_equal_list): + mocked_dispatch = mocker.patch("funkwhale_api.federation.routes.inbox.dispatch") + + r1 = factories["users.User"](with_actor=True).actor + r2 = factories["users.User"](with_actor=True).actor + a = factories["federation.Activity"](payload={"hello": "world"}) + ii1 = factories["federation.InboxItem"](activity=a, actor=r1) + ii2 = factories["federation.InboxItem"](activity=a, actor=r2) + tasks.dispatch_inbox(activity_id=a.pk) + + mocked_dispatch.assert_called_once_with( + a.payload, context={"actor": a.actor, "activity": a, "inbox_items": [ii1, ii2]} + ) + + +def test_dispatch_outbox(factories, mocker): + mocked_inbox = mocker.patch("funkwhale_api.federation.tasks.dispatch_inbox.delay") + mocked_deliver_to_remote = mocker.patch( + "funkwhale_api.federation.tasks.deliver_to_remote.delay" + ) + activity = factories["federation.Activity"](actor__local=True) + factories["federation.InboxItem"](activity=activity) + delivery = factories["federation.Delivery"](activity=activity) + tasks.dispatch_outbox(activity_id=activity.pk) + mocked_inbox.assert_called_once_with(activity_id=activity.pk) + mocked_deliver_to_remote.assert_called_once_with(delivery_id=delivery.pk) + + +def test_dispatch_outbox_disabled_federation(factories, mocker, preferences): + preferences["federation__enabled"] = False + mocked_inbox = mocker.patch("funkwhale_api.federation.tasks.dispatch_inbox.delay") + mocked_deliver_to_remote = mocker.patch( + "funkwhale_api.federation.tasks.deliver_to_remote.delay" + ) + activity = factories["federation.Activity"](actor__local=True) + factories["federation.InboxItem"](activity=activity) + factories["federation.Delivery"](activity=activity) + tasks.dispatch_outbox(activity_id=activity.pk) + mocked_inbox.assert_called_once_with(activity_id=activity.pk) + mocked_deliver_to_remote.assert_not_called() + + +def test_deliver_to_remote_success_mark_as_delivered(factories, r_mock, now): + delivery = factories["federation.Delivery"]() + r_mock.post(delivery.inbox_url) + tasks.deliver_to_remote(delivery_id=delivery.pk) + + delivery.refresh_from_db() + + request = r_mock.request_history[0] + assert delivery.is_delivered is True + assert delivery.attempts == 1 + assert delivery.last_attempt_date == now + assert r_mock.called is True + assert r_mock.call_count == 1 + assert request.url == delivery.inbox_url + assert request.headers["content-type"] == "application/activity+json" + assert request.json() == delivery.activity.payload + + +def test_deliver_to_remote_error(factories, r_mock, now): + delivery = factories["federation.Delivery"]() + r_mock.post(delivery.inbox_url, status_code=404) + + with pytest.raises(tasks.RequestException): + tasks.deliver_to_remote(delivery_id=delivery.pk) + + delivery.refresh_from_db() + + assert delivery.is_delivered is False + assert delivery.attempts == 1 + assert delivery.last_attempt_date == now diff --git a/api/tests/federation/test_utils.py b/api/tests/federation/test_utils.py index dbebe0fdc..e89c52543 100644 --- a/api/tests/federation/test_utils.py +++ b/api/tests/federation/test_utils.py @@ -1,3 +1,4 @@ +from rest_framework import serializers import pytest from funkwhale_api.federation import utils @@ -50,3 +51,41 @@ def test_extract_headers_from_meta(): "User-Agent": "http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)", } assert cleaned_headers == expected + + +def test_retrieve(r_mock): + fid = "https://some.url" + m = r_mock.get(fid, json={"hello": "world"}) + result = utils.retrieve(fid) + + assert result == {"hello": "world"} + assert m.request_history[-1].headers["Accept"] == "application/activity+json" + + +def test_retrieve_with_actor(r_mock, factories): + actor = factories["federation.Actor"]() + fid = "https://some.url" + m = r_mock.get(fid, json={"hello": "world"}) + result = utils.retrieve(fid, actor=actor) + + assert result == {"hello": "world"} + assert m.request_history[-1].headers["Accept"] == "application/activity+json" + assert m.request_history[-1].headers["Signature"] is not None + + +def test_retrieve_with_queryset(factories): + actor = factories["federation.Actor"]() + + assert utils.retrieve(actor.fid, queryset=actor.__class__) + + +def test_retrieve_with_serializer(r_mock): + class S(serializers.Serializer): + def create(self, validated_data): + return {"persisted": "object"} + + fid = "https://some.url" + r_mock.get(fid, json={"hello": "world"}) + result = utils.retrieve(fid, serializer_class=S) + + assert result == {"persisted": "object"} diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py index 4f1f471d8..2caa7856a 100644 --- a/api/tests/federation/test_views.py +++ b/api/tests/federation/test_views.py @@ -1,61 +1,8 @@ import pytest from django.core.paginator import Paginator from django.urls import reverse -from django.utils import timezone -from funkwhale_api.federation import ( - activity, - actors, - models, - serializers, - utils, - views, - webfinger, -) -from funkwhale_api.music import tasks as music_tasks - - -@pytest.mark.parametrize( - "view,permissions", - [ - (views.LibraryViewSet, ["federation"]), - (views.LibraryTrackViewSet, ["federation"]), - ], -) -def test_permissions(assert_user_permission, view, permissions): - assert_user_permission(view, permissions) - - -@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys()) -def test_instance_actors(system_actor, db, api_client): - actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance() - url = reverse("federation:instance-actors-detail", kwargs={"actor": system_actor}) - response = api_client.get(url) - serializer = serializers.ActorSerializer(actor) - - if system_actor == "library": - response.data.pop("url") - assert response.status_code == 200 - assert response.data == serializer.data - - -@pytest.mark.parametrize( - "route,kwargs", - [ - ("instance-actors-outbox", {"actor": "library"}), - ("instance-actors-inbox", {"actor": "library"}), - ("instance-actors-detail", {"actor": "library"}), - ("well-known-webfinger", {}), - ], -) -def test_instance_endpoints_405_if_federation_disabled( - authenticated_actor, db, preferences, api_client, route, kwargs -): - preferences["federation__enabled"] = False - url = reverse("federation:{}".format(route), kwargs=kwargs) - response = api_client.get(url) - - assert response.status_code == 405 +from funkwhale_api.federation import serializers, webfinger def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker): @@ -69,22 +16,6 @@ def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker assert response.data["errors"]["resource"] == ("Missing webfinger resource type") -@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys()) -def test_wellknown_webfinger_system(system_actor, db, api_client, settings, mocker): - actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance() - url = reverse("federation:well-known-webfinger") - response = api_client.get( - url, - data={"resource": "acct:{}".format(actor.webfinger_subject)}, - HTTP_ACCEPT="application/jrd+json", - ) - serializer = serializers.ActorWebfingerSerializer(actor) - - assert response.status_code == 200 - assert response["Content-Type"] == "application/jrd+json" - assert response.data == serializer.data - - def test_wellknown_nodeinfo(db, preferences, api_client, settings): expected = { "links": [ @@ -110,321 +41,12 @@ def test_wellknown_nodeinfo_disabled(db, preferences, api_client): assert response.status_code == 404 -def test_audio_file_list_requires_authenticated_actor(db, preferences, api_client): - preferences["federation__music_needs_approval"] = True - url = reverse("federation:music:files-list") - response = api_client.get(url) - - assert response.status_code == 403 - - -def test_audio_file_list_actor_no_page(db, preferences, api_client, factories): - preferences["federation__music_needs_approval"] = False - preferences["federation__collection_page_size"] = 2 - library = actors.SYSTEM_ACTORS["library"].get_actor_instance() - tfs = factories["music.TrackFile"].create_batch(size=5) - conf = { - "id": utils.full_url(reverse("federation:music:files-list")), - "page_size": 2, - "items": list(reversed(tfs)), # we order by -creation_date - "item_serializer": serializers.AudioSerializer, - "actor": library, - } - expected = serializers.PaginatedCollectionSerializer(conf).data - url = reverse("federation:music:files-list") - response = api_client.get(url) - - assert response.status_code == 200 - assert response.data == expected - - -def test_audio_file_list_actor_page(db, preferences, api_client, factories): - preferences["federation__music_needs_approval"] = False - preferences["federation__collection_page_size"] = 2 - library = actors.SYSTEM_ACTORS["library"].get_actor_instance() - tfs = factories["music.TrackFile"].create_batch(size=5) - conf = { - "id": utils.full_url(reverse("federation:music:files-list")), - "page": Paginator(list(reversed(tfs)), 2).page(2), - "item_serializer": serializers.AudioSerializer, - "actor": library, - } - expected = serializers.CollectionPageSerializer(conf).data - url = reverse("federation:music:files-list") - response = api_client.get(url, data={"page": 2}) - - assert response.status_code == 200 - assert response.data == expected - - -def test_audio_file_list_actor_page_exclude_federated_files( - db, preferences, api_client, factories -): - preferences["federation__music_needs_approval"] = False - factories["music.TrackFile"].create_batch(size=5, federation=True) - - url = reverse("federation:music:files-list") - response = api_client.get(url) - - assert response.status_code == 200 - assert response.data["totalItems"] == 0 - - -def test_audio_file_list_actor_page_error(db, preferences, api_client, factories): - preferences["federation__music_needs_approval"] = False - url = reverse("federation:music:files-list") - response = api_client.get(url, data={"page": "nope"}) - - assert response.status_code == 400 - - -def test_audio_file_list_actor_page_error_too_far( - db, preferences, api_client, factories -): - preferences["federation__music_needs_approval"] = False - url = reverse("federation:music:files-list") - response = api_client.get(url, data={"page": 5000}) - - assert response.status_code == 404 - - -def test_library_actor_includes_library_link(db, preferences, api_client): - url = reverse("federation:instance-actors-detail", kwargs={"actor": "library"}) - response = api_client.get(url) - expected_links = [ - { - "type": "Link", - "name": "library", - "mediaType": "application/activity+json", - "href": utils.full_url(reverse("federation:music:files-list")), - } - ] - assert response.status_code == 200 - assert response.data["url"] == expected_links - - -def test_can_fetch_library(superuser_api_client, mocker): - result = {"test": "test"} - scan = mocker.patch( - "funkwhale_api.federation.library.scan_from_account_name", return_value=result - ) - - url = reverse("api:v1:federation:libraries-fetch") - response = superuser_api_client.get(url, data={"account": "test@test.library"}) - - assert response.status_code == 200 - assert response.data == result - scan.assert_called_once_with("test@test.library") - - -def test_follow_library(superuser_api_client, mocker, factories, r_mock): - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - actor = factories["federation.Actor"]() - follow = {"test": "follow"} - on_commit = mocker.patch("funkwhale_api.common.utils.on_commit") - actor_data = serializers.ActorSerializer(actor).data - actor_data["url"] = [ - {"href": "https://test.library", "name": "library", "type": "Link"} - ] - library_conf = { - "id": "https://test.library", - "items": range(10), - "actor": actor, - "page_size": 5, - } - library_data = serializers.PaginatedCollectionSerializer(library_conf).data - r_mock.get(actor.url, json=actor_data) - r_mock.get("https://test.library", json=library_data) - data = { - "actor": actor.url, - "autoimport": False, - "federation_enabled": True, - "download_files": False, - } - - url = reverse("api:v1:federation:libraries-list") - response = superuser_api_client.post(url, data) - - assert response.status_code == 201 - - follow = models.Follow.objects.get(actor=library_actor, target=actor, approved=None) - library = follow.library - - assert response.data == serializers.APILibraryCreateSerializer(library).data - - on_commit.assert_called_once_with( - activity.deliver, - serializers.FollowSerializer(follow).data, - on_behalf_of=library_actor, - to=[actor.url], - ) - - -def test_can_list_system_actor_following(factories, superuser_api_client): - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - follow1 = factories["federation.Follow"](actor=library_actor) - factories["federation.Follow"]() - - url = reverse("api:v1:federation:libraries-following") - response = superuser_api_client.get(url) - - assert response.status_code == 200 - assert response.data["results"] == [serializers.APIFollowSerializer(follow1).data] - - -def test_can_list_system_actor_followers(factories, superuser_api_client): - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - factories["federation.Follow"](actor=library_actor) - follow2 = factories["federation.Follow"](target=library_actor) - - url = reverse("api:v1:federation:libraries-followers") - response = superuser_api_client.get(url) - - assert response.status_code == 200 - assert response.data["results"] == [serializers.APIFollowSerializer(follow2).data] - - -def test_can_list_libraries(factories, superuser_api_client): - library1 = factories["federation.Library"]() - library2 = factories["federation.Library"]() - - url = reverse("api:v1:federation:libraries-list") - response = superuser_api_client.get(url) - - assert response.status_code == 200 - assert response.data["results"] == [ - serializers.APILibrarySerializer(library1).data, - serializers.APILibrarySerializer(library2).data, - ] - - -def test_can_detail_library(factories, superuser_api_client): - library = factories["federation.Library"]() - - url = reverse( - "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)} - ) - response = superuser_api_client.get(url) - - assert response.status_code == 200 - assert response.data == serializers.APILibrarySerializer(library).data - - -def test_can_patch_library(factories, superuser_api_client): - library = factories["federation.Library"]() - data = { - "federation_enabled": not library.federation_enabled, - "download_files": not library.download_files, - "autoimport": not library.autoimport, - } - url = reverse( - "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)} - ) - response = superuser_api_client.patch(url, data) - - assert response.status_code == 200 - library.refresh_from_db() - - for k, v in data.items(): - assert getattr(library, k) == v - - -def test_scan_library(factories, mocker, superuser_api_client): - scan = mocker.patch( - "funkwhale_api.federation.tasks.scan_library.delay", - return_value=mocker.Mock(id="id"), - ) - library = factories["federation.Library"]() - now = timezone.now() - data = {"until": now} - url = reverse( - "api:v1:federation:libraries-scan", kwargs={"uuid": str(library.uuid)} - ) - response = superuser_api_client.post(url, data) - - assert response.status_code == 200 - assert response.data == {"task": "id"} - scan.assert_called_once_with(library_id=library.pk, until=now) - - -def test_list_library_tracks(factories, superuser_api_client): - library = factories["federation.Library"]() - lts = list( - reversed( - factories["federation.LibraryTrack"].create_batch(size=5, library=library) - ) - ) - factories["federation.LibraryTrack"].create_batch(size=5) - url = reverse("api:v1:federation:library-tracks-list") - response = superuser_api_client.get(url, {"library": library.uuid}) - - assert response.status_code == 200 - assert response.data == { - "results": serializers.APILibraryTrackSerializer(lts, many=True).data, - "count": 5, - "previous": None, - "next": None, - } - - -def test_can_update_follow_status(factories, superuser_api_client, mocker): - patched_accept = mocker.patch("funkwhale_api.federation.activity.accept_follow") - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - follow = factories["federation.Follow"](target=library_actor) - - payload = {"follow": follow.pk, "approved": True} - url = reverse("api:v1:federation:libraries-followers") - response = superuser_api_client.patch(url, payload) - follow.refresh_from_db() - - assert response.status_code == 200 - assert follow.approved is True - patched_accept.assert_called_once_with(follow) - - -def test_can_filter_pending_follows(factories, superuser_api_client): - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - factories["federation.Follow"](target=library_actor, approved=True) - - params = {"pending": True} - url = reverse("api:v1:federation:libraries-followers") - response = superuser_api_client.get(url, params) - - assert response.status_code == 200 - assert len(response.data["results"]) == 0 - - -def test_library_track_action_import(factories, superuser_api_client, mocker): - lt1 = factories["federation.LibraryTrack"]() - lt2 = factories["federation.LibraryTrack"](library=lt1.library) - lt3 = factories["federation.LibraryTrack"]() - factories["federation.LibraryTrack"](library=lt3.library) - mocked_run = mocker.patch("funkwhale_api.common.utils.on_commit") - - payload = { - "objects": "all", - "action": "import", - "filters": {"library": lt1.library.uuid}, - } - url = reverse("api:v1:federation:library-tracks-action") - response = superuser_api_client.post(url, payload, format="json") - batch = superuser_api_client.user.imports.latest("id") - expected = {"updated": 2, "action": "import", "result": {"batch": {"id": batch.pk}}} - - imported_lts = [lt1, lt2] - assert response.status_code == 200 - assert response.data == expected - assert batch.jobs.count() == 2 - for i, job in enumerate(batch.jobs.all()): - assert job.library_track == imported_lts[i] - mocked_run.assert_called_once_with( - music_tasks.import_batch_run.delay, import_batch_id=batch.pk - ) - - def test_local_actor_detail(factories, api_client): user = factories["users.User"](with_actor=True) - url = reverse("federation:actors-detail", kwargs={"user__username": user.username}) + url = reverse( + "federation:actors-detail", + kwargs={"preferred_username": user.actor.preferred_username}, + ) serializer = serializers.ActorSerializer(user.actor) response = api_client.get(url) @@ -432,6 +54,43 @@ def test_local_actor_detail(factories, api_client): assert response.data == serializer.data +def test_local_actor_inbox_post_requires_auth(factories, api_client): + user = factories["users.User"](with_actor=True) + url = reverse( + "federation:actors-inbox", + kwargs={"preferred_username": user.actor.preferred_username}, + ) + response = api_client.post(url, {"hello": "world"}) + + assert response.status_code == 403 + + +def test_local_actor_inbox_post(factories, api_client, mocker, authenticated_actor): + patched_receive = mocker.patch("funkwhale_api.federation.activity.receive") + user = factories["users.User"](with_actor=True) + url = reverse( + "federation:actors-inbox", + kwargs={"preferred_username": user.actor.preferred_username}, + ) + response = api_client.post(url, {"hello": "world"}, format="json") + + assert response.status_code == 200 + patched_receive.assert_called_once_with( + activity={"hello": "world"}, on_behalf_of=authenticated_actor + ) + + +def test_shared_inbox_post(factories, api_client, mocker, authenticated_actor): + patched_receive = mocker.patch("funkwhale_api.federation.activity.receive") + url = reverse("federation:shared-inbox") + response = api_client.post(url, {"hello": "world"}, format="json") + + assert response.status_code == 200 + patched_receive.assert_called_once_with( + activity={"hello": "world"}, on_behalf_of=authenticated_actor + ) + + def test_wellknown_webfinger_local(factories, api_client, settings, mocker): user = factories["users.User"](with_actor=True) url = reverse("federation:well-known-webfinger") @@ -445,3 +104,60 @@ def test_wellknown_webfinger_local(factories, api_client, settings, mocker): assert response.status_code == 200 assert response["Content-Type"] == "application/jrd+json" assert response.data == serializer.data + + +@pytest.mark.parametrize("privacy_level", ["me", "instance", "everyone"]) +def test_music_library_retrieve(factories, api_client, privacy_level): + library = factories["music.Library"](privacy_level=privacy_level) + expected = serializers.LibrarySerializer(library).data + + url = reverse("federation:music:libraries-detail", kwargs={"uuid": library.uuid}) + response = api_client.get(url) + + assert response.status_code == 200 + assert response.data == expected + + +def test_music_library_retrieve_page_public(factories, api_client): + library = factories["music.Library"](privacy_level="everyone") + upload = factories["music.Upload"](library=library, import_status="finished") + id = library.get_federation_id() + expected = serializers.CollectionPageSerializer( + { + "id": id, + "item_serializer": serializers.UploadSerializer, + "actor": library.actor, + "page": Paginator([upload], 1).page(1), + "name": library.name, + "summary": library.description, + } + ).data + + url = reverse("federation:music:libraries-detail", kwargs={"uuid": library.uuid}) + response = api_client.get(url, {"page": 1}) + + assert response.status_code == 200 + assert response.data == expected + + +@pytest.mark.parametrize("privacy_level", ["me", "instance"]) +def test_music_library_retrieve_page_private(factories, api_client, privacy_level): + library = factories["music.Library"](privacy_level=privacy_level) + url = reverse("federation:music:libraries-detail", kwargs={"uuid": library.uuid}) + response = api_client.get(url, {"page": 1}) + + assert response.status_code == 403 + + +@pytest.mark.parametrize("approved,expected", [(True, 200), (False, 403)]) +def test_music_library_retrieve_page_follow( + factories, api_client, authenticated_actor, approved, expected +): + library = factories["music.Library"](privacy_level="me") + factories["federation.LibraryFollow"]( + actor=authenticated_actor, target=library, approved=approved + ) + url = reverse("federation:music:libraries-detail", kwargs={"uuid": library.uuid}) + response = api_client.get(url, {"page": 1}) + + assert response.status_code == expected diff --git a/api/tests/instance/test_nodeinfo.py b/api/tests/instance/test_nodeinfo.py index 181ddf277..0fa1b4fc3 100644 --- a/api/tests/instance/test_nodeinfo.py +++ b/api/tests/instance/test_nodeinfo.py @@ -1,4 +1,3 @@ - import funkwhale_api from funkwhale_api.instance import nodeinfo diff --git a/api/tests/instance/test_stats.py b/api/tests/instance/test_stats.py index 1d8bcfc0a..4820735d5 100644 --- a/api/tests/instance/test_stats.py +++ b/api/tests/instance/test_stats.py @@ -8,7 +8,7 @@ def test_get_users(mocker): def test_get_music_duration(factories): - factories["music.TrackFile"].create_batch(size=5, duration=360) + factories["music.Upload"].create_batch(size=5, duration=360) # duration is in hours assert stats.get_music_duration() == 0.5 diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 9742b098d..df55ab823 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -1,13 +1,13 @@ from funkwhale_api.manage import serializers -def test_manage_track_file_action_delete(factories): - tfs = factories["music.TrackFile"](size=5) - s = serializers.ManageTrackFileActionSerializer(queryset=None) +def test_manage_upload_action_delete(factories): + uploads = factories["music.Upload"](size=5) + s = serializers.ManageUploadActionSerializer(queryset=None) - s.handle_delete(tfs.__class__.objects.all()) + s.handle_delete(uploads.__class__.objects.all()) - assert tfs.__class__.objects.count() == 0 + assert uploads.__class__.objects.count() == 0 def test_user_update_permission(factories): @@ -20,55 +20,19 @@ def test_user_update_permission(factories): ) s = serializers.ManageUserSerializer( user, - data={"is_active": False, "permissions": {"federation": False, "upload": True}}, + data={ + "is_active": False, + "permissions": {"federation": False, "upload": True}, + "upload_quota": 12, + }, ) s.is_valid(raise_exception=True) s.save() user.refresh_from_db() assert user.is_active is False + assert user.upload_quota == 12 assert user.permission_federation is False assert user.permission_upload is True assert user.permission_library is False assert user.permission_settings is True - - -def test_manage_import_request_mark_closed(factories): - affected = factories["requests.ImportRequest"].create_batch( - size=5, status="pending" - ) - # we do not update imported requests - factories["requests.ImportRequest"].create_batch(size=5, status="imported") - s = serializers.ManageImportRequestActionSerializer( - queryset=affected[0].__class__.objects.all(), - data={"objects": "all", "action": "mark_closed"}, - ) - - assert s.is_valid(raise_exception=True) is True - s.save() - - assert affected[0].__class__.objects.filter(status="imported").count() == 5 - for ir in affected: - ir.refresh_from_db() - assert ir.status == "closed" - - -def test_manage_import_request_mark_imported(factories, now): - affected = factories["requests.ImportRequest"].create_batch( - size=5, status="pending" - ) - # we do not update closed requests - factories["requests.ImportRequest"].create_batch(size=5, status="closed") - s = serializers.ManageImportRequestActionSerializer( - queryset=affected[0].__class__.objects.all(), - data={"objects": "all", "action": "mark_imported"}, - ) - - assert s.is_valid(raise_exception=True) is True - s.save() - - assert affected[0].__class__.objects.filter(status="closed").count() == 5 - for ir in affected: - ir.refresh_from_db() - assert ir.status == "imported" - assert ir.imported_date == now diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index baf816fc8..a9920ce07 100644 --- a/api/tests/manage/test_views.py +++ b/api/tests/manage/test_views.py @@ -7,27 +7,27 @@ from funkwhale_api.manage import serializers, views @pytest.mark.parametrize( "view,permissions,operator", [ - (views.ManageTrackFileViewSet, ["library"], "and"), + (views.ManageUploadViewSet, ["library"], "and"), (views.ManageUserViewSet, ["settings"], "and"), (views.ManageInvitationViewSet, ["settings"], "and"), - (views.ManageImportRequestViewSet, ["library"], "and"), ], ) def test_permissions(assert_user_permission, view, permissions, operator): assert_user_permission(view, permissions, operator) -def test_track_file_view(factories, superuser_api_client): - tfs = factories["music.TrackFile"].create_batch(size=5) - qs = tfs[0].__class__.objects.order_by("-creation_date") - url = reverse("api:v1:manage:library:track-files-list") +@pytest.mark.skip(reason="Refactoring in progress") +def test_upload_view(factories, superuser_api_client): + uploads = factories["music.Upload"].create_batch(size=5) + qs = uploads[0].__class__.objects.order_by("-creation_date") + url = reverse("api:v1:manage:library:uploads-list") response = superuser_api_client.get(url, {"sort": "-creation_date"}) - expected = serializers.ManageTrackFileSerializer( + expected = serializers.ManageUploadSerializer( qs, many=True, context={"request": response.wsgi_request} ).data - assert response.data["count"] == len(tfs) + assert response.data["count"] == len(uploads) assert response.data["results"] == expected @@ -64,15 +64,3 @@ def test_invitation_view_create(factories, superuser_api_client, mocker): assert response.status_code == 201 assert superuser_api_client.user.invitations.latest("id") is not None - - -def test_music_requests_view(factories, superuser_api_client, mocker): - invitations = factories["requests.ImportRequest"].create_batch(size=5) - qs = invitations[0].__class__.objects.order_by("-id") - url = reverse("api:v1:manage:requests:import-requests-list") - - response = superuser_api_client.get(url, {"sort": "-id"}) - expected = serializers.ManageImportRequestSerializer(qs, many=True).data - - assert response.data["count"] == len(invitations) - assert response.data["results"] == expected diff --git a/api/tests/music/test.mp3 b/api/tests/music/test.mp3 index 8502de71b..6c1f52a35 100644 Binary files a/api/tests/music/test.mp3 and b/api/tests/music/test.mp3 differ diff --git a/api/tests/music/test.opus b/api/tests/music/test.opus new file mode 100644 index 000000000..c3ca732b3 Binary files /dev/null and b/api/tests/music/test.opus differ diff --git a/api/tests/music/test_activity.py b/api/tests/music/test_activity.py index 8c119394d..f19936362 100644 --- a/api/tests/music/test_activity.py +++ b/api/tests/music/test_activity.py @@ -1,3 +1,7 @@ +from funkwhale_api.music import serializers +from funkwhale_api.music import signals + + def test_get_track_activity_url_mbid(factories): track = factories["music.Track"]() expected = "https://musicbrainz.org/recording/{}".format(track.mbid) @@ -8,3 +12,27 @@ def test_get_track_activity_url_no_mbid(settings, factories): track = factories["music.Track"](mbid=None) expected = settings.FUNKWHALE_URL + "/tracks/{}".format(track.pk) assert track.get_activity_url() == expected + + +def test_upload_import_status_updated_broadcast(factories, mocker): + group_send = mocker.patch("funkwhale_api.common.channels.group_send") + user = factories["users.User"]() + upload = factories["music.Upload"]( + import_status="finished", library__actor__user=user + ) + signals.upload_import_status_updated.send( + sender=None, upload=upload, old_status="pending", new_status="finished" + ) + group_send.assert_called_once_with( + "user.{}.imports".format(user.pk), + { + "type": "event.send", + "text": "", + "data": { + "type": "import.status_updated", + "old_status": "pending", + "new_status": "finished", + "upload": serializers.UploadForOwnerSerializer(upload).data, + }, + }, + ) diff --git a/api/tests/music/test_api.py b/api/tests/music/test_api.py index 29a712ce6..99130c9d9 100644 --- a/api/tests/music/test_api.py +++ b/api/tests/music/test_api.py @@ -1,217 +1,12 @@ -import json import os import pytest from django.urls import reverse -from funkwhale_api.music import models, tasks DATA_DIR = os.path.dirname(os.path.abspath(__file__)) -def test_can_submit_youtube_url_for_track_import( - settings, artists, albums, tracks, mocker, superuser_client -): - mocker.patch("funkwhale_api.music.tasks.import_job_run.delay") - mocker.patch( - "funkwhale_api.musicbrainz.api.artists.get", - return_value=artists["get"]["adhesive_wombat"], - ) - mocker.patch( - "funkwhale_api.musicbrainz.api.releases.get", - return_value=albums["get"]["marsupial"], - ) - mocker.patch( - "funkwhale_api.musicbrainz.api.recordings.get", - return_value=tracks["get"]["8bitadventures"], - ) - mocker.patch( - "funkwhale_api.music.models.TrackFile.download_file", return_value=None - ) - mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed" - video_id = "tPEE9ZwTmy0" - url = reverse("api:v1:submit-single") - video_url = "https://www.youtube.com/watch?v={0}".format(video_id) - response = superuser_client.post(url, {"import_url": video_url, "mbid": mbid}) - - assert response.status_code == 201 - batch = superuser_client.user.imports.latest("id") - job = batch.jobs.latest("id") - assert job.status == "pending" - assert str(job.mbid) == mbid - assert job.source == video_url - - -def test_import_creates_an_import_with_correct_data(mocker, superuser_client): - mocker.patch("funkwhale_api.music.tasks.import_job_run") - mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed" - video_id = "tPEE9ZwTmy0" - url = reverse("api:v1:submit-single") - superuser_client.post( - url, - { - "import_url": "https://www.youtube.com/watch?v={0}".format(video_id), - "mbid": mbid, - }, - ) - - batch = models.ImportBatch.objects.latest("id") - assert batch.jobs.count() == 1 - assert batch.submitted_by == superuser_client.user - assert batch.status == "pending" - job = batch.jobs.first() - assert str(job.mbid) == mbid - assert job.status == "pending" - assert job.source == "https://www.youtube.com/watch?v={0}".format(video_id) - - -def test_can_import_whole_album(artists, albums, mocker, superuser_client): - mocker.patch("funkwhale_api.music.tasks.import_job_run") - mocker.patch( - "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["soad"] - ) - mocker.patch("funkwhale_api.musicbrainz.api.images.get_front", return_value=b"") - mocker.patch( - "funkwhale_api.musicbrainz.api.releases.get", - return_value=albums["get_with_includes"]["hypnotize"], - ) - payload = { - "releaseId": "47ae093f-1607-49a3-be11-a15d335ccc94", - "tracks": [ - { - "mbid": "1968a9d6-8d92-4051-8f76-674e157b6eed", - "source": "https://www.youtube.com/watch?v=1111111111", - }, - { - "mbid": "2968a9d6-8d92-4051-8f76-674e157b6eed", - "source": "https://www.youtube.com/watch?v=2222222222", - }, - { - "mbid": "3968a9d6-8d92-4051-8f76-674e157b6eed", - "source": "https://www.youtube.com/watch?v=3333333333", - }, - ], - } - url = reverse("api:v1:submit-album") - superuser_client.post(url, json.dumps(payload), content_type="application/json") - - batch = models.ImportBatch.objects.latest("id") - assert batch.jobs.count() == 3 - assert batch.submitted_by == superuser_client.user - assert batch.status == "pending" - - album = models.Album.objects.latest("id") - assert str(album.mbid) == "47ae093f-1607-49a3-be11-a15d335ccc94" - medium_data = albums["get_with_includes"]["hypnotize"]["release"]["medium-list"][0] - assert int(medium_data["track-count"]) == album.tracks.all().count() - - for track in medium_data["track-list"]: - instance = models.Track.objects.get(mbid=track["recording"]["id"]) - assert instance.title == track["recording"]["title"] - assert instance.position == int(track["position"]) - assert instance.title == track["recording"]["title"] - - for row in payload["tracks"]: - job = models.ImportJob.objects.get(mbid=row["mbid"]) - assert str(job.mbid) == row["mbid"] - assert job.status == "pending" - assert job.source == row["source"] - - -def test_can_import_whole_artist(artists, albums, mocker, superuser_client): - mocker.patch("funkwhale_api.music.tasks.import_job_run") - mocker.patch( - "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["soad"] - ) - mocker.patch("funkwhale_api.musicbrainz.api.images.get_front", return_value=b"") - mocker.patch( - "funkwhale_api.musicbrainz.api.releases.get", - return_value=albums["get_with_includes"]["hypnotize"], - ) - payload = { - "artistId": "mbid", - "albums": [ - { - "releaseId": "47ae093f-1607-49a3-be11-a15d335ccc94", - "tracks": [ - { - "mbid": "1968a9d6-8d92-4051-8f76-674e157b6eed", - "source": "https://www.youtube.com/watch?v=1111111111", - }, - { - "mbid": "2968a9d6-8d92-4051-8f76-674e157b6eed", - "source": "https://www.youtube.com/watch?v=2222222222", - }, - { - "mbid": "3968a9d6-8d92-4051-8f76-674e157b6eed", - "source": "https://www.youtube.com/watch?v=3333333333", - }, - ], - } - ], - } - url = reverse("api:v1:submit-artist") - superuser_client.post(url, json.dumps(payload), content_type="application/json") - - batch = models.ImportBatch.objects.latest("id") - assert batch.jobs.count() == 3 - assert batch.submitted_by == superuser_client.user - assert batch.status == "pending" - - album = models.Album.objects.latest("id") - assert str(album.mbid) == "47ae093f-1607-49a3-be11-a15d335ccc94" - medium_data = albums["get_with_includes"]["hypnotize"]["release"]["medium-list"][0] - assert int(medium_data["track-count"]) == album.tracks.all().count() - - for track in medium_data["track-list"]: - instance = models.Track.objects.get(mbid=track["recording"]["id"]) - assert instance.title == track["recording"]["title"] - assert instance.position == int(track["position"]) - assert instance.title == track["recording"]["title"] - - for row in payload["albums"][0]["tracks"]: - job = models.ImportJob.objects.get(mbid=row["mbid"]) - assert str(job.mbid) == row["mbid"] - assert job.status == "pending" - assert job.source == row["source"] - - -def test_user_can_create_an_empty_batch(superuser_api_client, factories): - url = reverse("api:v1:import-batches-list") - response = superuser_api_client.post(url) - - assert response.status_code == 201 - - batch = superuser_api_client.user.imports.latest("id") - - assert batch.submitted_by == superuser_api_client.user - assert batch.source == "api" - - -def test_user_can_create_import_job_with_file(superuser_api_client, factories, mocker): - path = os.path.join(DATA_DIR, "test.ogg") - m = mocker.patch("funkwhale_api.common.utils.on_commit") - batch = factories["music.ImportBatch"](submitted_by=superuser_api_client.user) - url = reverse("api:v1:import-jobs-list") - with open(path, "rb") as f: - content = f.read() - f.seek(0) - response = superuser_api_client.post( - url, {"batch": batch.pk, "audio_file": f, "source": "file://"} - ) - - assert response.status_code == 201 - - job = batch.jobs.latest("id") - - assert job.status == "pending" - assert job.source.startswith("file://") - assert "test.ogg" in job.source - assert job.audio_file.read() == content - - m.assert_called_once_with(tasks.import_job_run.delay, import_job_id=job.pk) - - @pytest.mark.parametrize( "route,method", [ @@ -230,25 +25,26 @@ def test_can_restrict_api_views_to_authenticated_users( assert response.status_code == 401 -def test_track_file_url_is_restricted_to_authenticated_users( +def test_upload_url_is_restricted_to_authenticated_users( api_client, factories, preferences ): preferences["common__api_authentication_required"] = True - f = factories["music.TrackFile"]() - assert f.audio_file is not None - url = f.path + upload = factories["music.Upload"](library__privacy_level="instance") + assert upload.audio_file is not None + url = upload.track.listen_url response = api_client.get(url) assert response.status_code == 401 -def test_track_file_url_is_accessible_to_authenticated_users( +def test_upload_url_is_accessible_to_authenticated_users( logged_in_api_client, factories, preferences ): + actor = logged_in_api_client.user.create_actor() preferences["common__api_authentication_required"] = True - f = factories["music.TrackFile"]() - assert f.audio_file is not None - url = f.path + upload = factories["music.Upload"](library__actor=actor, import_status="finished") + assert upload.audio_file is not None + url = upload.track.listen_url response = logged_in_api_client.get(url) assert response.status_code == 200 - assert response["X-Accel-Redirect"] == "/_protected{}".format(f.audio_file.url) + assert response["X-Accel-Redirect"] == "/_protected{}".format(upload.audio_file.url) diff --git a/api/tests/music/test_commands.py b/api/tests/music/test_commands.py index 03a9420dc..38186dd7e 100644 --- a/api/tests/music/test_commands.py +++ b/api/tests/music/test_commands.py @@ -1,14 +1,14 @@ import os -from funkwhale_api.music.management.commands import fix_track_files +from funkwhale_api.music.management.commands import fix_uploads DATA_DIR = os.path.dirname(os.path.abspath(__file__)) -def test_fix_track_files_bitrate_length(factories, mocker): - tf1 = factories["music.TrackFile"](bitrate=1, duration=2) - tf2 = factories["music.TrackFile"](bitrate=None, duration=None) - c = fix_track_files.Command() +def test_fix_uploads_bitrate_length(factories, mocker): + upload1 = factories["music.Upload"](bitrate=1, duration=2) + upload2 = factories["music.Upload"](bitrate=None, duration=None) + c = fix_uploads.Command() mocker.patch( "funkwhale_api.music.utils.get_audio_file_data", @@ -17,57 +17,59 @@ def test_fix_track_files_bitrate_length(factories, mocker): c.fix_file_data(dry_run=False) - tf1.refresh_from_db() - tf2.refresh_from_db() + upload1.refresh_from_db() + upload2.refresh_from_db() # not updated - assert tf1.bitrate == 1 - assert tf1.duration == 2 + assert upload1.bitrate == 1 + assert upload1.duration == 2 # updated - assert tf2.bitrate == 42 - assert tf2.duration == 43 + assert upload2.bitrate == 42 + assert upload2.duration == 43 -def test_fix_track_files_size(factories, mocker): - tf1 = factories["music.TrackFile"](size=1) - tf2 = factories["music.TrackFile"](size=None) - c = fix_track_files.Command() +def test_fix_uploads_size(factories, mocker): + upload1 = factories["music.Upload"]() + upload2 = factories["music.Upload"]() + upload1.__class__.objects.filter(pk=upload1.pk).update(size=1) + upload2.__class__.objects.filter(pk=upload2.pk).update(size=None) + c = fix_uploads.Command() - mocker.patch("funkwhale_api.music.models.TrackFile.get_file_size", return_value=2) + mocker.patch("funkwhale_api.music.models.Upload.get_file_size", return_value=2) c.fix_file_size(dry_run=False) - tf1.refresh_from_db() - tf2.refresh_from_db() + upload1.refresh_from_db() + upload2.refresh_from_db() # not updated - assert tf1.size == 1 + assert upload1.size == 1 # updated - assert tf2.size == 2 + assert upload2.size == 2 -def test_fix_track_files_mimetype(factories, mocker): +def test_fix_uploads_mimetype(factories, mocker): mp3_path = os.path.join(DATA_DIR, "test.mp3") ogg_path = os.path.join(DATA_DIR, "test.ogg") - tf1 = factories["music.TrackFile"]( + upload1 = factories["music.Upload"]( audio_file__from_path=mp3_path, source="file://{}".format(mp3_path), mimetype="application/x-empty", ) # this one already has a mimetype set, to it should not be updated - tf2 = factories["music.TrackFile"]( + upload2 = factories["music.Upload"]( audio_file__from_path=ogg_path, source="file://{}".format(ogg_path), mimetype="audio/something", ) - c = fix_track_files.Command() + c = fix_uploads.Command() c.fix_mimetypes(dry_run=False) - tf1.refresh_from_db() - tf2.refresh_from_db() + upload1.refresh_from_db() + upload2.refresh_from_db() - assert tf1.mimetype == "audio/mpeg" - assert tf2.mimetype == "audio/something" + assert upload1.mimetype == "audio/mpeg" + assert upload2.mimetype == "audio/something" diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py index 12185f888..1c1e5fea5 100644 --- a/api/tests/music/test_import.py +++ b/api/tests/music/test_import.py @@ -1,249 +1,15 @@ -import json import os import pytest import uuid from django import forms -from django.urls import reverse -from funkwhale_api.federation import actors -from funkwhale_api.federation import serializers as federation_serializers from funkwhale_api.music import importers from funkwhale_api.music import models -from funkwhale_api.music import tasks DATA_DIR = os.path.dirname(os.path.abspath(__file__)) -def test_create_import_can_bind_to_request( - artists, albums, mocker, factories, superuser_api_client -): - request = factories["requests.ImportRequest"]() - - mocker.patch("funkwhale_api.music.tasks.import_job_run") - mocker.patch( - "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["soad"] - ) - mocker.patch("funkwhale_api.musicbrainz.api.images.get_front", return_value=b"") - mocker.patch( - "funkwhale_api.musicbrainz.api.releases.get", - return_value=albums["get_with_includes"]["hypnotize"], - ) - payload = { - "releaseId": "47ae093f-1607-49a3-be11-a15d335ccc94", - "importRequest": request.pk, - "tracks": [ - { - "mbid": "1968a9d6-8d92-4051-8f76-674e157b6eed", - "source": "https://www.youtube.com/watch?v=1111111111", - } - ], - } - url = reverse("api:v1:submit-album") - superuser_api_client.post(url, json.dumps(payload), content_type="application/json") - batch = request.import_batches.latest("id") - - assert batch.import_request == request - - -def test_import_job_from_federation_no_musicbrainz(factories, mocker): - mocker.patch( - "funkwhale_api.music.utils.get_audio_file_data", - return_value={"bitrate": 24, "length": 666}, - ) - mocker.patch("funkwhale_api.music.models.TrackFile.get_file_size", return_value=42) - lt = factories["federation.LibraryTrack"]( - artist_name="Hello", - album_title="World", - title="Ping", - metadata__length=42, - metadata__bitrate=43, - metadata__size=44, - ) - job = factories["music.ImportJob"](federation=True, library_track=lt) - - tasks.import_job_run(import_job_id=job.pk) - job.refresh_from_db() - - tf = job.track_file - assert tf.mimetype == lt.audio_mimetype - assert tf.duration == 42 - assert tf.bitrate == 43 - assert tf.size == 44 - assert tf.library_track == job.library_track - assert tf.track.title == "Ping" - assert tf.track.artist.name == "Hello" - assert tf.track.album.title == "World" - - -def test_import_job_from_federation_musicbrainz_recording(factories, mocker): - t = factories["music.Track"]() - track_from_api = mocker.patch( - "funkwhale_api.music.models.Track.get_or_create_from_api", - return_value=(t, True), - ) - lt = factories["federation.LibraryTrack"]( - metadata__recording__musicbrainz=True, artist_name="Hello", album_title="World" - ) - job = factories["music.ImportJob"](federation=True, library_track=lt) - - tasks.import_job_run(import_job_id=job.pk) - job.refresh_from_db() - - tf = job.track_file - assert tf.mimetype == lt.audio_mimetype - assert tf.library_track == job.library_track - assert tf.track == t - track_from_api.assert_called_once_with( - mbid=lt.metadata["recording"]["musicbrainz_id"] - ) - - -def test_import_job_from_federation_musicbrainz_release(factories, mocker): - a = factories["music.Album"]() - album_from_api = mocker.patch( - "funkwhale_api.music.models.Album.get_or_create_from_api", - return_value=(a, True), - ) - lt = factories["federation.LibraryTrack"]( - metadata__release__musicbrainz=True, artist_name="Hello", title="Ping" - ) - job = factories["music.ImportJob"](federation=True, library_track=lt) - - tasks.import_job_run(import_job_id=job.pk) - job.refresh_from_db() - - tf = job.track_file - assert tf.mimetype == lt.audio_mimetype - assert tf.library_track == job.library_track - assert tf.track.title == "Ping" - assert tf.track.artist == a.artist - assert tf.track.album == a - - album_from_api.assert_called_once_with( - mbid=lt.metadata["release"]["musicbrainz_id"] - ) - - -def test_import_job_from_federation_musicbrainz_artist(factories, mocker): - a = factories["music.Artist"]() - artist_from_api = mocker.patch( - "funkwhale_api.music.models.Artist.get_or_create_from_api", - return_value=(a, True), - ) - lt = factories["federation.LibraryTrack"]( - metadata__artist__musicbrainz=True, album_title="World", title="Ping" - ) - job = factories["music.ImportJob"](federation=True, library_track=lt) - - tasks.import_job_run(import_job_id=job.pk) - job.refresh_from_db() - - tf = job.track_file - assert tf.mimetype == lt.audio_mimetype - assert tf.library_track == job.library_track - - assert tf.track.title == "Ping" - assert tf.track.artist == a - assert tf.track.album.artist == a - assert tf.track.album.title == "World" - - artist_from_api.assert_called_once_with( - mbid=lt.metadata["artist"]["musicbrainz_id"] - ) - - -def test_import_job_run_triggers_notifies_followers(factories, mocker, tmpfile): - mocker.patch( - "funkwhale_api.downloader.download", - return_value={"audio_file_path": tmpfile.name}, - ) - mocked_notify = mocker.patch( - "funkwhale_api.music.tasks.import_batch_notify_followers.delay" - ) - batch = factories["music.ImportBatch"]() - job = factories["music.ImportJob"](finished=True, batch=batch) - factories["music.Track"](mbid=job.mbid) - - batch.update_status() - batch.refresh_from_db() - - assert batch.status == "finished" - - mocked_notify.assert_called_once_with(import_batch_id=batch.pk) - - -def test_import_batch_notifies_followers_skip_on_disabled_federation( - preferences, factories, mocker -): - mocked_deliver = mocker.patch("funkwhale_api.federation.activity.deliver") - batch = factories["music.ImportBatch"](finished=True) - preferences["federation__enabled"] = False - tasks.import_batch_notify_followers(import_batch_id=batch.pk) - - mocked_deliver.assert_not_called() - - -def test_import_batch_notifies_followers_skip_on_federation_import(factories, mocker): - mocked_deliver = mocker.patch("funkwhale_api.federation.activity.deliver") - batch = factories["music.ImportBatch"](finished=True, federation=True) - tasks.import_batch_notify_followers(import_batch_id=batch.pk) - - mocked_deliver.assert_not_called() - - -def test_import_batch_notifies_followers(factories, mocker): - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - - f1 = factories["federation.Follow"](approved=True, target=library_actor) - factories["federation.Follow"](approved=False, target=library_actor) - factories["federation.Follow"]() - - mocked_deliver = mocker.patch("funkwhale_api.federation.activity.deliver") - batch = factories["music.ImportBatch"]() - job1 = factories["music.ImportJob"](finished=True, batch=batch) - factories["music.ImportJob"](finished=True, federation=True, batch=batch) - factories["music.ImportJob"](status="pending", batch=batch) - - batch.status = "finished" - batch.save() - tasks.import_batch_notify_followers(import_batch_id=batch.pk) - - # only f1 match the requirements to be notified - # and only job1 is a non federated track with finished import - expected = { - "@context": federation_serializers.AP_CONTEXT, - "actor": library_actor.url, - "type": "Create", - "id": batch.get_federation_url(), - "to": [f1.actor.url], - "object": federation_serializers.CollectionSerializer( - { - "id": batch.get_federation_url(), - "items": [job1.track_file], - "actor": library_actor, - "item_serializer": federation_serializers.AudioSerializer, - } - ).data, - } - - mocked_deliver.assert_called_once_with( - expected, on_behalf_of=library_actor, to=[f1.actor.url] - ) - - -def test__do_import_in_place_mbid(factories, tmpfile): - path = os.path.join(DATA_DIR, "test.ogg") - job = factories["music.ImportJob"](in_place=True, source="file://{}".format(path)) - - factories["music.Track"](mbid=job.mbid) - tf = tasks._do_import(job, use_acoustid=False) - - assert bool(tf.audio_file) is False - assert tf.source == "file://{}".format(path) - assert tf.mimetype == "audio/ogg" - - def test_importer_cleans(): importer = importers.Importer(models.Artist) with pytest.raises(forms.ValidationError): diff --git a/api/tests/music/test_metadata.py b/api/tests/music/test_metadata.py index 1dfe927a0..82c991c0b 100644 --- a/api/tests/music/test_metadata.py +++ b/api/tests/music/test_metadata.py @@ -9,17 +9,42 @@ from funkwhale_api.music import metadata DATA_DIR = os.path.dirname(os.path.abspath(__file__)) +def test_get_all_metadata_at_once(): + path = os.path.join(DATA_DIR, "test.ogg") + data = metadata.Metadata(path) + + expected = { + "title": "Peer Gynt Suite no. 1, op. 46: I. Morning", + "artist": "Edvard Grieg", + "album_artist": "Edvard Grieg", + "album": "Peer Gynt Suite no. 1, op. 46", + "date": datetime.date(2012, 8, 15), + "track_number": 1, + "musicbrainz_albumid": uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75"), + "musicbrainz_recordingid": uuid.UUID("bd21ac48-46d8-4e78-925f-d9cc2a294656"), + "musicbrainz_artistid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"), + "musicbrainz_albumartistid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"), + } + + assert data.all() == expected + + @pytest.mark.parametrize( "field,value", [ ("title", "Peer Gynt Suite no. 1, op. 46: I. Morning"), ("artist", "Edvard Grieg"), + ("album_artist", "Edvard Grieg"), ("album", "Peer Gynt Suite no. 1, op. 46"), ("date", datetime.date(2012, 8, 15)), ("track_number", 1), ("musicbrainz_albumid", uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75")), ("musicbrainz_recordingid", uuid.UUID("bd21ac48-46d8-4e78-925f-d9cc2a294656")), ("musicbrainz_artistid", uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823")), + ( + "musicbrainz_albumartistid", + uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"), + ), ], ) def test_can_get_metadata_from_ogg_file(field, value): @@ -29,17 +54,47 @@ def test_can_get_metadata_from_ogg_file(field, value): assert data.get(field) == value +@pytest.mark.parametrize( + "field,value", + [ + ("title", "Peer Gynt Suite no. 1, op. 46: I. Morning"), + ("artist", "Edvard Grieg"), + ("album_artist", "Edvard Grieg"), + ("album", "Peer Gynt Suite no. 1, op. 46"), + ("date", datetime.date(2012, 8, 15)), + ("track_number", 1), + ("musicbrainz_albumid", uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75")), + ("musicbrainz_recordingid", uuid.UUID("bd21ac48-46d8-4e78-925f-d9cc2a294656")), + ("musicbrainz_artistid", uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823")), + ( + "musicbrainz_albumartistid", + uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"), + ), + ], +) +def test_can_get_metadata_from_opus_file(field, value): + path = os.path.join(DATA_DIR, "test.opus") + data = metadata.Metadata(path) + + assert data.get(field) == value + + @pytest.mark.parametrize( "field,value", [ ("title", "Drei Kreuze (dass wir hier sind)"), ("artist", "Die Toten Hosen"), + ("album_artist", "Die Toten Hosen"), ("album", "Ballast der Republik"), ("date", datetime.date(2012, 5, 4)), ("track_number", 1), ("musicbrainz_albumid", uuid.UUID("1f0441ad-e609-446d-b355-809c445773cf")), ("musicbrainz_recordingid", uuid.UUID("124d0150-8627-46bc-bc14-789a3bc960c8")), ("musicbrainz_artistid", uuid.UUID("c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1")), + ( + "musicbrainz_albumartistid", + uuid.UUID("c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"), + ), ], ) def test_can_get_metadata_from_ogg_theora_file(field, value): @@ -53,13 +108,18 @@ def test_can_get_metadata_from_ogg_theora_file(field, value): "field,value", [ ("title", "Bend"), - ("artist", "Bindrpilot"), + ("artist", "Binärpilot"), + ("album_artist", "Binärpilot"), ("album", "You Can't Stop Da Funk"), ("date", datetime.date(2006, 2, 7)), ("track_number", 2), ("musicbrainz_albumid", uuid.UUID("ce40cdb1-a562-4fd8-a269-9269f98d4124")), ("musicbrainz_recordingid", uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcb")), ("musicbrainz_artistid", uuid.UUID("9c6bddde-6228-4d9f-ad0d-03f6fcb19e13")), + ( + "musicbrainz_albumartistid", + uuid.UUID("9c6bddde-6228-4d9f-ad0d-03f6fcb19e13"), + ), ], ) def test_can_get_metadata_from_id3_mp3_file(field, value): @@ -88,12 +148,17 @@ def test_can_get_pictures(name): [ ("title", "999,999"), ("artist", "Nine Inch Nails"), + ("album_artist", "Nine Inch Nails"), ("album", "The Slip"), ("date", datetime.date(2008, 5, 5)), ("track_number", 1), ("musicbrainz_albumid", uuid.UUID("12b57d46-a192-499e-a91f-7da66790a1c1")), ("musicbrainz_recordingid", uuid.UUID("30f3f33e-8d0c-4e69-8539-cbd701d18f28")), ("musicbrainz_artistid", uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da")), + ( + "musicbrainz_albumartistid", + uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da"), + ), ], ) def test_can_get_metadata_from_flac_file(field, value): @@ -113,7 +178,12 @@ def test_can_get_metadata_from_flac_file_not_crash_if_empty(): @pytest.mark.parametrize( "field_name", - ["musicbrainz_artistid", "musicbrainz_albumid", "musicbrainz_recordingid"], + [ + "musicbrainz_artistid", + "musicbrainz_albumid", + "musicbrainz_recordingid", + "musicbrainz_albumartistid", + ], ) def test_mbid_clean_keeps_only_first(field_name): u1 = str(uuid.uuid4()) diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py index 1bd4282fe..d045a04c2 100644 --- a/api/tests/music/test_models.py +++ b/api/tests/music/test_models.py @@ -2,7 +2,11 @@ import os import pytest +from django.utils import timezone +from django.urls import reverse + from funkwhale_api.music import importers, models, tasks +from funkwhale_api.federation import utils as federation_utils DATA_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -148,29 +152,6 @@ def test_import_track_with_different_artist_than_release(factories, mocker): assert track.artist == artist -def test_import_job_is_bound_to_track_file(factories, mocker): - track = factories["music.Track"]() - job = factories["music.ImportJob"](mbid=track.mbid) - - mocker.patch("funkwhale_api.music.models.TrackFile.download_file") - tasks.import_job_run(import_job_id=job.pk) - job.refresh_from_db() - assert job.track_file.track == track - - -@pytest.mark.parametrize("status", ["pending", "errored", "finished"]) -def test_saving_job_updates_batch_status(status, factories, mocker): - batch = factories["music.ImportBatch"]() - - assert batch.status == "pending" - - factories["music.ImportJob"](batch=batch, status=status) - - batch.refresh_from_db() - - assert batch.status == status - - @pytest.mark.parametrize( "extention,mimetype", [("ogg", "audio/ogg"), ("mp3", "audio/mpeg")] ) @@ -178,41 +159,33 @@ def test_audio_track_mime_type(extention, mimetype, factories): name = ".".join(["test", extention]) path = os.path.join(DATA_DIR, name) - tf = factories["music.TrackFile"](audio_file__from_path=path) + upload = factories["music.Upload"](audio_file__from_path=path, mimetype=None) - assert tf.mimetype == mimetype + assert upload.mimetype == mimetype -def test_track_file_file_name(factories): +def test_upload_file_name(factories): name = "test.mp3" path = os.path.join(DATA_DIR, name) - tf = factories["music.TrackFile"](audio_file__from_path=path) + upload = factories["music.Upload"](audio_file__from_path=path) - assert tf.filename == tf.track.full_name + ".mp3" + assert upload.filename == upload.track.full_name + ".mp3" def test_track_get_file_size(factories): name = "test.mp3" path = os.path.join(DATA_DIR, name) - tf = factories["music.TrackFile"](audio_file__from_path=path) + upload = factories["music.Upload"](audio_file__from_path=path) - assert tf.get_file_size() == 297745 - - -def test_track_get_file_size_federation(factories): - tf = factories["music.TrackFile"]( - federation=True, library_track__with_audio_file=True - ) - - assert tf.get_file_size() == tf.library_track.audio_file.size + assert upload.get_file_size() == 297745 def test_track_get_file_size_in_place(factories): name = "test.mp3" path = os.path.join(DATA_DIR, name) - tf = factories["music.TrackFile"](in_place=True, source="file://{}".format(path)) + upload = factories["music.Upload"](in_place=True, source="file://{}".format(path)) - assert tf.get_file_size() == 297745 + assert upload.get_file_size() == 297745 def test_album_get_image_content(factories): @@ -221,3 +194,311 @@ def test_album_get_image_content(factories): album.refresh_from_db() assert album.cover.read() == b"test" + + +def test_library(factories): + now = timezone.now() + actor = factories["federation.Actor"]() + library = factories["music.Library"]( + name="Hello world", description="hello", actor=actor, privacy_level="instance" + ) + + assert library.creation_date >= now + assert library.uploads.count() == 0 + assert library.uuid is not None + + +@pytest.mark.parametrize( + "status,expected", [("pending", False), ("errored", False), ("finished", True)] +) +def test_playable_by_correct_status(status, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level="everyone", import_status=status + ) + queryset = upload.library.uploads.playable_by(None) + match = upload in list(queryset) + assert match is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", True), ("instance", True), ("everyone", True)] +) +def test_playable_by_correct_actor(privacy_level, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level=privacy_level, import_status="finished" + ) + queryset = upload.library.uploads.playable_by(upload.library.actor) + match = upload in list(queryset) + assert match is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", True), ("everyone", True)] +) +def test_playable_by_instance_actor(privacy_level, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level=privacy_level, import_status="finished" + ) + instance_actor = factories["federation.Actor"](domain=upload.library.actor.domain) + queryset = upload.library.uploads.playable_by(instance_actor) + match = upload in list(queryset) + assert match is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", False), ("everyone", True)] +) +def test_playable_by_anonymous(privacy_level, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level=privacy_level, import_status="finished" + ) + queryset = upload.library.uploads.playable_by(None) + match = upload in list(queryset) + assert match is expected + + +@pytest.mark.parametrize("approved", [True, False]) +def test_playable_by_follower(approved, factories): + upload = factories["music.Upload"]( + library__privacy_level="me", import_status="finished" + ) + actor = factories["federation.Actor"](local=True) + factories["federation.LibraryFollow"]( + target=upload.library, actor=actor, approved=approved + ) + queryset = upload.library.uploads.playable_by(actor) + match = upload in list(queryset) + expected = approved + assert match is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", True), ("instance", True), ("everyone", True)] +) +def test_track_playable_by_correct_actor(privacy_level, expected, factories): + upload = factories["music.Upload"](import_status="finished") + queryset = models.Track.objects.playable_by( + upload.library.actor + ).annotate_playable_by_actor(upload.library.actor) + match = upload.track in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", True), ("everyone", True)] +) +def test_track_playable_by_instance_actor(privacy_level, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level=privacy_level, import_status="finished" + ) + instance_actor = factories["federation.Actor"](domain=upload.library.actor.domain) + queryset = models.Track.objects.playable_by( + instance_actor + ).annotate_playable_by_actor(instance_actor) + match = upload.track in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", False), ("everyone", True)] +) +def test_track_playable_by_anonymous(privacy_level, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level=privacy_level, import_status="finished" + ) + queryset = models.Track.objects.playable_by(None).annotate_playable_by_actor(None) + match = upload.track in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", True), ("instance", True), ("everyone", True)] +) +def test_album_playable_by_correct_actor(privacy_level, expected, factories): + upload = factories["music.Upload"](import_status="finished") + + queryset = models.Album.objects.playable_by( + upload.library.actor + ).annotate_playable_by_actor(upload.library.actor) + match = upload.track.album in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", True), ("everyone", True)] +) +def test_album_playable_by_instance_actor(privacy_level, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level=privacy_level, import_status="finished" + ) + instance_actor = factories["federation.Actor"](domain=upload.library.actor.domain) + queryset = models.Album.objects.playable_by( + instance_actor + ).annotate_playable_by_actor(instance_actor) + match = upload.track.album in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", False), ("everyone", True)] +) +def test_album_playable_by_anonymous(privacy_level, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level=privacy_level, import_status="finished" + ) + queryset = models.Album.objects.playable_by(None).annotate_playable_by_actor(None) + match = upload.track.album in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", True), ("instance", True), ("everyone", True)] +) +def test_artist_playable_by_correct_actor(privacy_level, expected, factories): + upload = factories["music.Upload"](import_status="finished") + + queryset = models.Artist.objects.playable_by( + upload.library.actor + ).annotate_playable_by_actor(upload.library.actor) + match = upload.track.artist in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", True), ("everyone", True)] +) +def test_artist_playable_by_instance_actor(privacy_level, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level=privacy_level, import_status="finished" + ) + instance_actor = factories["federation.Actor"](domain=upload.library.actor.domain) + queryset = models.Artist.objects.playable_by( + instance_actor + ).annotate_playable_by_actor(instance_actor) + match = upload.track.artist in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", False), ("everyone", True)] +) +def test_artist_playable_by_anonymous(privacy_level, expected, factories): + upload = factories["music.Upload"]( + library__privacy_level=privacy_level, import_status="finished" + ) + queryset = models.Artist.objects.playable_by(None).annotate_playable_by_actor(None) + match = upload.track.artist in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +def test_upload_listen_url(factories): + upload = factories["music.Upload"]() + expected = upload.track.listen_url + "?upload={}".format(upload.uuid) + + assert upload.listen_url == expected + + +def test_library_schedule_scan(factories, now, mocker): + on_commit = mocker.patch("funkwhale_api.common.utils.on_commit") + library = factories["music.Library"](uploads_count=5) + + scan = library.schedule_scan(library.actor) + + assert scan.creation_date >= now + assert scan.status == "pending" + assert scan.library == library + assert scan.actor == library.actor + assert scan.total_files == 5 + assert scan.processed_files == 0 + assert scan.errored_files == 0 + assert scan.modification_date is None + + on_commit.assert_called_once_with( + tasks.start_library_scan.delay, library_scan_id=scan.pk + ) + + +def test_library_schedule_scan_too_recent(factories, now): + scan = factories["music.LibraryScan"]() + result = scan.library.schedule_scan(scan.library.actor) + + assert result is None + assert scan.library.scans.count() == 1 + + +def test_get_audio_data(factories): + upload = factories["music.Upload"]() + + result = upload.get_audio_data() + + assert result == {"duration": 229, "bitrate": 128000, "size": 3459481} + + +def test_library_queryset_with_follows(factories): + library1 = factories["music.Library"]() + library2 = factories["music.Library"]() + follow = factories["federation.LibraryFollow"](target=library2) + qs = library1.__class__.objects.with_follows(follow.actor).order_by("pk") + + l1 = list(qs)[0] + l2 = list(qs)[1] + assert l1._follows == [] + assert l2._follows == [follow] + + +def test_annotate_duration(factories): + tf = factories["music.Upload"](duration=32) + + track = models.Track.objects.annotate_duration().get(pk=tf.track.pk) + + assert track.duration == 32 + + +def test_annotate_file_data(factories): + tf = factories["music.Upload"](size=42, bitrate=55, mimetype="audio/ogg") + + track = models.Track.objects.annotate_file_data().get(pk=tf.track.pk) + + assert track.size == 42 + assert track.bitrate == 55 + assert track.mimetype == "audio/ogg" + + +@pytest.mark.parametrize( + "model,factory_args,namespace", + [ + ( + "music.Upload", + {"library__actor__local": True}, + "federation:music:uploads-detail", + ), + ("music.Library", {"actor__local": True}, "federation:music:libraries-detail"), + ("music.Artist", {}, "federation:music:artists-detail"), + ("music.Album", {}, "federation:music:albums-detail"), + ("music.Track", {}, "federation:music:tracks-detail"), + ], +) +def test_fid_is_populated(factories, model, factory_args, namespace): + instance = factories[model](**factory_args, fid=None) + + assert instance.fid == federation_utils.full_url( + reverse(namespace, kwargs={"uuid": instance.uuid}) + ) diff --git a/api/tests/music/test_music.py b/api/tests/music/test_music.py index 387cebb2c..ab5853f14 100644 --- a/api/tests/music/test_music.py +++ b/api/tests/music/test_music.py @@ -2,6 +2,7 @@ import datetime import pytest +from funkwhale_api.federation import utils as federation_utils from funkwhale_api.music import models @@ -17,6 +18,9 @@ def test_can_create_artist_from_api(artists, mocker, db): assert data["id"], "62c3befb-6366-4585-b256-809472333801" assert artist.mbid, data["id"] assert artist.name, "Adhesive Wombat" + assert artist.fid == federation_utils.full_url( + "/federation/music/artists/{}".format(artist.uuid) + ) def test_can_create_album_from_api(artists, albums, mocker, db): @@ -41,6 +45,9 @@ def test_can_create_album_from_api(artists, albums, mocker, db): assert album.release_date, datetime.date(2005, 1, 1) assert album.artist.name, "System of a Down" assert album.artist.mbid, data["artist-credit"][0]["artist"]["id"] + assert album.fid == federation_utils.full_url( + "/federation/music/albums/{}".format(album.uuid) + ) def test_can_create_track_from_api(artists, albums, tracks, mocker, db): @@ -66,6 +73,9 @@ def test_can_create_track_from_api(artists, albums, tracks, mocker, db): assert track.artist.name == "Adhesive Wombat" assert str(track.album.mbid) == "a50d2a81-2a50-484d-9cb4-b9f6833f583e" assert track.album.title == "Marsupial Madness" + assert track.fid == federation_utils.full_url( + "/federation/music/tracks/{}".format(track.uuid) + ) def test_can_create_track_from_api_with_corresponding_tags( diff --git a/api/tests/music/test_permissions.py b/api/tests/music/test_permissions.py deleted file mode 100644 index 5f73a361e..000000000 --- a/api/tests/music/test_permissions.py +++ /dev/null @@ -1,60 +0,0 @@ -from rest_framework.views import APIView - -from funkwhale_api.federation import actors -from funkwhale_api.music import permissions - - -def test_list_permission_no_protect(preferences, anonymous_user, api_request): - preferences["common__api_authentication_required"] = False - view = APIView.as_view() - permission = permissions.Listen() - request = api_request.get("/") - assert permission.has_permission(request, view) is True - - -def test_list_permission_protect_authenticated(factories, api_request, preferences): - preferences["common__api_authentication_required"] = True - user = factories["users.User"]() - view = APIView.as_view() - permission = permissions.Listen() - request = api_request.get("/") - setattr(request, "user", user) - assert permission.has_permission(request, view) is True - - -def test_list_permission_protect_not_following_actor( - factories, api_request, preferences -): - preferences["common__api_authentication_required"] = True - actor = factories["federation.Actor"]() - view = APIView.as_view() - permission = permissions.Listen() - request = api_request.get("/") - setattr(request, "actor", actor) - assert permission.has_permission(request, view) is False - - -def test_list_permission_protect_following_actor(factories, api_request, preferences): - preferences["common__api_authentication_required"] = True - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - follow = factories["federation.Follow"](approved=True, target=library_actor) - view = APIView.as_view() - permission = permissions.Listen() - request = api_request.get("/") - setattr(request, "actor", follow.actor) - - assert permission.has_permission(request, view) is True - - -def test_list_permission_protect_following_actor_not_approved( - factories, api_request, preferences -): - preferences["common__api_authentication_required"] = True - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() - follow = factories["federation.Follow"](approved=False, target=library_actor) - view = APIView.as_view() - permission = permissions.Listen() - request = api_request.get("/") - setattr(request, "actor", follow.actor) - - assert permission.has_permission(request, view) is False diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py index 8705354f7..330371834 100644 --- a/api/tests/music/test_serializers.py +++ b/api/tests/music/test_serializers.py @@ -1,4 +1,6 @@ +from funkwhale_api.music import models from funkwhale_api.music import serializers +from funkwhale_api.music import tasks def test_artist_album_serializer(factories, to_api_date): @@ -12,6 +14,7 @@ def test_artist_album_serializer(factories, to_api_date): "artist": album.artist.id, "creation_date": to_api_date(album.creation_date), "tracks_count": 1, + "is_playable": None, "cover": { "original": album.cover.url, "square_crop": album.cover.crop["400x400"].url, @@ -43,8 +46,8 @@ def test_artist_with_albums_serializer(factories, to_api_date): def test_album_track_serializer(factories, to_api_date): - tf = factories["music.TrackFile"]() - track = tf.track + upload = factories["music.Upload"]() + track = upload.track expected = { "id": track.id, @@ -53,28 +56,64 @@ def test_album_track_serializer(factories, to_api_date): "mbid": str(track.mbid), "title": track.title, "position": track.position, + "is_playable": None, "creation_date": to_api_date(track.creation_date), - "files": [serializers.TrackFileSerializer(tf).data], + "listen_url": track.listen_url, + "duration": None, } serializer = serializers.AlbumTrackSerializer(track) assert serializer.data == expected -def test_track_file_serializer(factories, to_api_date): - tf = factories["music.TrackFile"]() +def test_upload_serializer(factories, to_api_date): + upload = factories["music.Upload"]() expected = { - "id": tf.id, - "path": tf.path, - "source": tf.source, - "filename": tf.filename, - "track": tf.track.pk, - "duration": tf.duration, - "mimetype": tf.mimetype, - "bitrate": tf.bitrate, - "size": tf.size, + "uuid": str(upload.uuid), + "filename": upload.filename, + "track": serializers.TrackSerializer(upload.track).data, + "duration": upload.duration, + "mimetype": upload.mimetype, + "bitrate": upload.bitrate, + "size": upload.size, + "library": serializers.LibraryForOwnerSerializer(upload.library).data, + "creation_date": upload.creation_date.isoformat().split("+")[0] + "Z", + "import_date": None, + "import_status": "pending", } - serializer = serializers.TrackFileSerializer(tf) + serializer = serializers.UploadSerializer(upload) + assert serializer.data == expected + + +def test_upload_owner_serializer(factories, to_api_date): + upload = factories["music.Upload"]( + import_status="success", + import_details={"hello": "world"}, + import_metadata={"import": "metadata"}, + import_reference="ref", + metadata={"test": "metadata"}, + source="upload://test", + ) + + expected = { + "uuid": str(upload.uuid), + "filename": upload.filename, + "track": serializers.TrackSerializer(upload.track).data, + "duration": upload.duration, + "mimetype": upload.mimetype, + "bitrate": upload.bitrate, + "size": upload.size, + "library": serializers.LibraryForOwnerSerializer(upload.library).data, + "creation_date": upload.creation_date.isoformat().split("+")[0] + "Z", + "metadata": {"test": "metadata"}, + "import_metadata": {"import": "metadata"}, + "import_date": None, + "import_status": "success", + "import_details": {"hello": "world"}, + "source": "upload://test", + "import_reference": "ref", + } + serializer = serializers.UploadForOwnerSerializer(upload) assert serializer.data == expected @@ -88,6 +127,7 @@ def test_album_serializer(factories, to_api_date): "title": album.title, "artist": serializers.ArtistSimpleSerializer(album.artist).data, "creation_date": to_api_date(album.creation_date), + "is_playable": None, "cover": { "original": album.cover.url, "square_crop": album.cover.crop["400x400"].url, @@ -103,8 +143,8 @@ def test_album_serializer(factories, to_api_date): def test_track_serializer(factories, to_api_date): - tf = factories["music.TrackFile"]() - track = tf.track + upload = factories["music.Upload"]() + track = upload.track expected = { "id": track.id, @@ -113,9 +153,110 @@ def test_track_serializer(factories, to_api_date): "mbid": str(track.mbid), "title": track.title, "position": track.position, + "is_playable": None, "creation_date": to_api_date(track.creation_date), "lyrics": track.get_lyrics_url(), - "files": [serializers.TrackFileSerializer(tf).data], + "listen_url": track.listen_url, + "duration": None, + "size": None, + "bitrate": None, + "mimetype": None, } serializer = serializers.TrackSerializer(track) assert serializer.data == expected + + +def test_user_cannot_bind_file_to_a_not_owned_library(factories): + user = factories["users.User"]() + library = factories["music.Library"]() + + s = serializers.UploadForOwnerSerializer( + data={"library": library.uuid, "source": "upload://test"}, + context={"user": user}, + ) + assert s.is_valid() is False + assert "library" in s.errors + + +def test_user_can_create_file_in_own_library(factories, uploaded_audio_file): + user = factories["users.User"]() + library = factories["music.Library"](actor__user=user) + s = serializers.UploadForOwnerSerializer( + data={ + "library": library.uuid, + "source": "upload://test", + "audio_file": uploaded_audio_file, + }, + context={"user": user}, + ) + assert s.is_valid(raise_exception=True) is True + upload = s.save() + + assert upload.library == library + + +def test_create_file_checks_for_user_quota( + factories, preferences, uploaded_audio_file, mocker +): + mocker.patch( + "funkwhale_api.users.models.User.get_quota_status", + return_value={"remaining": 0}, + ) + user = factories["users.User"]() + library = factories["music.Library"](actor__user=user) + s = serializers.UploadForOwnerSerializer( + data={ + "library": library.uuid, + "source": "upload://test", + "audio_file": uploaded_audio_file, + }, + context={"user": user}, + ) + assert s.is_valid() is False + assert s.errors["non_field_errors"] == ["upload_quota_reached"] + + +def test_manage_upload_action_delete(factories, queryset_equal_list, mocker): + dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch") + library1 = factories["music.Library"]() + library2 = factories["music.Library"]() + library1_uploads = factories["music.Upload"].create_batch(size=3, library=library1) + library2_uploads = factories["music.Upload"].create_batch(size=3, library=library2) + s = serializers.UploadActionSerializer(queryset=None) + + s.handle_delete(library1_uploads[0].__class__.objects.all()) + + assert library1_uploads[0].__class__.objects.count() == 0 + dispatch.assert_any_call( + {"type": "Delete", "object": {"type": "Audio"}}, + context={"uploads": library1_uploads}, + ) + dispatch.assert_any_call( + {"type": "Delete", "object": {"type": "Audio"}}, + context={"uploads": library2_uploads}, + ) + + +def test_manage_upload_action_relaunch_import(factories, mocker): + m = mocker.patch("funkwhale_api.common.utils.on_commit") + + # this one is finished and should stay as is + finished = factories["music.Upload"](import_status="finished") + + to_relaunch = [ + factories["music.Upload"](import_status="pending"), + factories["music.Upload"](import_status="skipped"), + factories["music.Upload"](import_status="errored"), + ] + s = serializers.UploadActionSerializer(queryset=None) + + s.handle_relaunch_import(models.Upload.objects.all()) + + for obj in to_relaunch: + obj.refresh_from_db() + assert obj.import_status == "pending" + m.assert_any_call(tasks.process_upload.delay, upload_id=obj.pk) + + finished.refresh_from_db() + assert finished.import_status == "finished" + assert m.call_count == 3 diff --git a/api/tests/music/test_tasks.py b/api/tests/music/test_tasks.py index e91594d47..efa0e801f 100644 --- a/api/tests/music/test_tasks.py +++ b/api/tests/music/test_tasks.py @@ -1,249 +1,373 @@ +import datetime +import io import os - import pytest +import uuid -from funkwhale_api.music import tasks +from django.core.paginator import Paginator +from django.utils import timezone + +from funkwhale_api.federation import serializers as federation_serializers +from funkwhale_api.music import metadata, signals, tasks DATA_DIR = os.path.dirname(os.path.abspath(__file__)) -def test_set_acoustid_on_track_file(factories, mocker, preferences): - preferences["providers_acoustid__api_key"] = "test" - track_file = factories["music.TrackFile"](acoustid_track_id=None) - id = "e475bf79-c1ce-4441-bed7-1e33f226c0a2" - payload = { - "results": [ - { - "id": id, - "recordings": [ - { - "artists": [ - { - "id": "9c6bddde-6228-4d9f-ad0d-03f6fcb19e13", - "name": "Binärpilot", - } - ], - "duration": 268, - "id": "f269d497-1cc0-4ae4-a0c4-157ec7d73fcb", - "title": "Bend", - } - ], - "score": 0.860825, - } - ], - "status": "ok", +# DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads") + + +def test_can_create_track_from_file_metadata_no_mbid(db, mocker): + metadata = { + "title": "Test track", + "artist": "Test artist", + "album": "Test album", + "date": datetime.date(2012, 8, 15), + "track_number": 4, } - m = mocker.patch("acoustid.match", return_value=payload) - r = tasks.set_acoustid_on_track_file(track_file_id=track_file.pk) - track_file.refresh_from_db() + mocker.patch("funkwhale_api.music.metadata.Metadata.all", return_value=metadata) - assert str(track_file.acoustid_track_id) == id - assert r == id - m.assert_called_once_with("test", track_file.audio_file.path, parse=False) + track = tasks.get_track_from_import_metadata(metadata) + + assert track.title == metadata["title"] + assert track.mbid is None + assert track.position == 4 + assert track.album.title == metadata["album"] + assert track.album.mbid is None + assert track.album.release_date == datetime.date(2012, 8, 15) + assert track.artist.name == metadata["artist"] + assert track.artist.mbid is None -def test_set_acoustid_on_track_file_required_high_score(factories, mocker): - track_file = factories["music.TrackFile"](acoustid_track_id=None) - payload = {"results": [{"score": 0.79}], "status": "ok"} - mocker.patch("acoustid.match", return_value=payload) - tasks.set_acoustid_on_track_file(track_file_id=track_file.pk) - track_file.refresh_from_db() +def test_can_create_track_from_file_metadata_mbid(factories, mocker): + metadata = { + "title": "Test track", + "artist": "Test artist", + "album_artist": "Test album artist", + "album": "Test album", + "date": datetime.date(2012, 8, 15), + "track_number": 4, + "musicbrainz_albumid": "ce40cdb1-a562-4fd8-a269-9269f98d4124", + "musicbrainz_recordingid": "f269d497-1cc0-4ae4-a0c4-157ec7d73fcb", + "musicbrainz_artistid": "9c6bddde-6228-4d9f-ad0d-03f6fcb19e13", + "musicbrainz_albumartistid": "9c6bddde-6478-4d9f-ad0d-03f6fcb19e13", + "cover_data": {"content": b"image_content", "mimetype": "image/png"}, + } - assert track_file.acoustid_track_id is None + mocker.patch("funkwhale_api.music.metadata.Metadata.all", return_value=metadata) + + track = tasks.get_track_from_import_metadata(metadata) + + assert track.title == metadata["title"] + assert track.mbid == metadata["musicbrainz_recordingid"] + assert track.position == 4 + assert track.album.title == metadata["album"] + assert track.album.mbid == metadata["musicbrainz_albumid"] + assert track.album.artist.mbid == metadata["musicbrainz_albumartistid"] + assert track.album.artist.name == metadata["album_artist"] + assert track.album.release_date == datetime.date(2012, 8, 15) + assert track.artist.name == metadata["artist"] + assert track.artist.mbid == metadata["musicbrainz_artistid"] -def test_import_batch_run(factories, mocker): - job = factories["music.ImportJob"]() - mocked_job_run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay") - tasks.import_batch_run(import_batch_id=job.batch.pk) - - mocked_job_run.assert_called_once_with(import_job_id=job.pk) - - -@pytest.mark.skip("Acoustid is disabled") -def test_import_job_can_run_with_file_and_acoustid( - artists, albums, tracks, preferences, factories, mocker +def test_can_create_track_from_file_metadata_mbid_existing_album_artist( + factories, mocker ): - preferences["providers_acoustid__api_key"] = "test" - path = os.path.join(DATA_DIR, "test.ogg") - mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed" - acoustid_payload = { - "results": [ - { - "id": "e475bf79-c1ce-4441-bed7-1e33f226c0a2", - "recordings": [{"duration": 268, "id": mbid}], - "score": 0.860825, - } - ], - "status": "ok", + artist = factories["music.Artist"]() + album = factories["music.Album"]() + metadata = { + "artist": "", + "album": "", + "title": "Hello", + "track_number": 4, + "musicbrainz_albumid": album.mbid, + "musicbrainz_recordingid": "f269d497-1cc0-4ae4-a0c4-157ec7d73fcb", + "musicbrainz_artistid": artist.mbid, + "musicbrainz_albumartistid": album.artist.mbid, } + + mocker.patch("funkwhale_api.music.metadata.Metadata.all", return_value=metadata) + + track = tasks.get_track_from_import_metadata(metadata) + + assert track.title == metadata["title"] + assert track.mbid == metadata["musicbrainz_recordingid"] + assert track.position == 4 + assert track.album == album + assert track.artist == artist + + +def test_can_create_track_from_file_metadata_fid_existing_album_artist( + factories, mocker +): + artist = factories["music.Artist"]() + album = factories["music.Album"]() + metadata = { + "artist": "", + "album": "", + "title": "Hello", + "track_number": 4, + "fid": "https://hello", + "album_fid": album.fid, + "artist_fid": artist.fid, + "album_artist_fid": album.artist.fid, + } + + mocker.patch("funkwhale_api.music.metadata.Metadata.all", return_value=metadata) + + track = tasks.get_track_from_import_metadata(metadata) + + assert track.title == metadata["title"] + assert track.fid == metadata["fid"] + assert track.position == 4 + assert track.album == album + assert track.artist == artist + + +def test_can_create_track_from_file_metadata_federation(factories, mocker, r_mock): + metadata = { + "artist": "Artist", + "album": "Album", + "album_artist": "Album artist", + "title": "Hello", + "track_number": 4, + "fid": "https://hello", + "album_fid": "https://album.fid", + "artist_fid": "https://artist.fid", + "album_artist_fid": "https://album.artist.fid", + "fdate": timezone.now(), + "album_fdate": timezone.now(), + "album_artist_fdate": timezone.now(), + "artist_fdate": timezone.now(), + "cover_data": {"url": "https://cover/hello.png", "mimetype": "image/png"}, + } + r_mock.get(metadata["cover_data"]["url"], body=io.BytesIO(b"coucou")) + mocker.patch("funkwhale_api.music.metadata.Metadata.all", return_value=metadata) + + track = tasks.get_track_from_import_metadata(metadata) + + assert track.title == metadata["title"] + assert track.fid == metadata["fid"] + assert track.creation_date == metadata["fdate"] + assert track.position == 4 + assert track.album.cover.read() == b"coucou" + assert track.album.cover.path.endswith(".png") + assert track.album.fid == metadata["album_fid"] + assert track.album.title == metadata["album"] + assert track.album.creation_date == metadata["album_fdate"] + assert track.album.artist.fid == metadata["album_artist_fid"] + assert track.album.artist.name == metadata["album_artist"] + assert track.album.artist.creation_date == metadata["album_artist_fdate"] + assert track.artist.fid == metadata["artist_fid"] + assert track.artist.name == metadata["artist"] + assert track.artist.creation_date == metadata["artist_fdate"] + + +def test_sort_candidates(factories): + artist1 = factories["music.Artist"].build(fid=None, mbid=None) + artist2 = factories["music.Artist"].build(fid=None) + artist3 = factories["music.Artist"].build(mbid=None) + result = tasks.sort_candidates([artist1, artist2, artist3], ["mbid", "fid"]) + + assert result == [artist2, artist3, artist1] + + +def test_upload_import(now, factories, temp_signal, mocker): + outbox = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch") + track = factories["music.Track"]() + upload = factories["music.Upload"]( + track=None, import_metadata={"funkwhale": {"track": {"uuid": str(track.uuid)}}} + ) + + with temp_signal(signals.upload_import_status_updated) as handler: + tasks.process_upload(upload_id=upload.pk) + + upload.refresh_from_db() + + assert upload.track == track + assert upload.import_status == "finished" + assert upload.import_date == now + handler.assert_called_once_with( + upload=upload, + old_status="pending", + new_status="finished", + sender=None, + signal=signals.upload_import_status_updated, + ) + outbox.assert_called_once_with( + {"type": "Create", "object": {"type": "Audio"}}, context={"upload": upload} + ) + + +def test_upload_import_get_audio_data(factories, mocker): mocker.patch( - "funkwhale_api.music.utils.get_audio_file_data", - return_value={"bitrate": 42, "length": 43}, + "funkwhale_api.music.models.Upload.get_audio_data", + return_value={"size": 23, "duration": 42, "bitrate": 66}, ) + track = factories["music.Track"]() + upload = factories["music.Upload"]( + track=None, import_metadata={"funkwhale": {"track": {"uuid": track.uuid}}} + ) + + tasks.process_upload(upload_id=upload.pk) + + upload.refresh_from_db() + assert upload.size == 23 + assert upload.duration == 42 + assert upload.bitrate == 66 + + +def test_upload_import_in_place(factories, mocker): mocker.patch( - "funkwhale_api.musicbrainz.api.artists.get", - return_value=artists["get"]["adhesive_wombat"], + "funkwhale_api.music.models.Upload.get_audio_data", + return_value={"size": 23, "duration": 42, "bitrate": 66}, ) - mocker.patch( - "funkwhale_api.musicbrainz.api.releases.get", - return_value=albums["get"]["marsupial"], - ) - mocker.patch( - "funkwhale_api.musicbrainz.api.recordings.search", - return_value=tracks["search"]["8bitadventures"], - ) - mocker.patch("acoustid.match", return_value=acoustid_payload) - - job = factories["music.FileImportJob"](audio_file__path=path) - f = job.audio_file - tasks.import_job_run(import_job_id=job.pk) - job.refresh_from_db() - - track_file = job.track_file - - with open(path, "rb") as f: - assert track_file.audio_file.read() == f.read() - assert track_file.bitrate == 42 - assert track_file.duration == 43 - assert track_file.size == os.path.getsize(path) - # audio file is deleted from import job once persisted to audio file - assert not job.audio_file - assert job.status == "finished" - assert job.source == "file://" - - -def test_run_import_skipping_accoustid(factories, mocker): - m = mocker.patch("funkwhale_api.music.tasks._do_import") + track = factories["music.Track"]() path = os.path.join(DATA_DIR, "test.ogg") - job = factories["music.FileImportJob"](audio_file__path=path) - tasks.import_job_run(import_job_id=job.pk, use_acoustid=False) - m.assert_called_once_with(job, use_acoustid=False) - - -def test__do_import_skipping_accoustid(factories, mocker): - t = factories["music.Track"]() - m = mocker.patch( - "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path", - return_value=t, - ) - path = os.path.join(DATA_DIR, "test.ogg") - job = factories["music.FileImportJob"](mbid=None, audio_file__path=path) - p = job.audio_file.path - tasks._do_import(job, use_acoustid=False) - m.assert_called_once_with(p) - - -def test__do_import_skipping_accoustid_if_no_key(factories, mocker, preferences): - preferences["providers_acoustid__api_key"] = "" - t = factories["music.Track"]() - m = mocker.patch( - "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path", - return_value=t, - ) - path = os.path.join(DATA_DIR, "test.ogg") - job = factories["music.FileImportJob"](mbid=None, audio_file__path=path) - p = job.audio_file.path - tasks._do_import(job, use_acoustid=False) - m.assert_called_once_with(p) - - -def test__do_import_replace_if_duplicate(factories, mocker): - existing_file = factories["music.TrackFile"]() - existing_track = existing_file.track - path = os.path.join(DATA_DIR, "test.ogg") - mocker.patch( - "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path", - return_value=existing_track, - ) - job = factories["music.FileImportJob"]( - replace_if_duplicate=True, audio_file__path=path - ) - tasks._do_import(job) - with pytest.raises(existing_file.__class__.DoesNotExist): - existing_file.refresh_from_db() - assert existing_file.creation_date != job.track_file.creation_date - - -def test_import_job_skip_if_already_exists(artists, albums, tracks, factories, mocker): - path = os.path.join(DATA_DIR, "test.ogg") - mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed" - track_file = factories["music.TrackFile"](track__mbid=mbid) - mocker.patch( - "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path", - return_value=track_file.track, + upload = factories["music.Upload"]( + track=None, + audio_file=None, + source="file://{}".format(path), + import_metadata={"funkwhale": {"track": {"uuid": track.uuid}}}, ) - job = factories["music.FileImportJob"](audio_file__path=path) - tasks.import_job_run(import_job_id=job.pk) - job.refresh_from_db() + tasks.process_upload(upload_id=upload.pk) - assert job.track_file is None - # audio file is deleted from import job once persisted to audio file - assert not job.audio_file - assert job.status == "skipped" + upload.refresh_from_db() + assert upload.size == 23 + assert upload.duration == 42 + assert upload.bitrate == 66 + assert upload.mimetype == "audio/ogg" -def test_import_job_can_be_errored(factories, mocker, preferences): - path = os.path.join(DATA_DIR, "test.ogg") - mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed" - factories["music.TrackFile"](track__mbid=mbid) +def test_upload_import_skip_existing_track_in_own_library(factories, temp_signal): + track = factories["music.Track"]() + library = factories["music.Library"]() + existing = factories["music.Upload"]( + track=track, + import_status="finished", + library=library, + import_metadata={"funkwhale": {"track": {"uuid": track.mbid}}}, + ) + duplicate = factories["music.Upload"]( + track=track, + import_status="pending", + library=library, + import_metadata={"funkwhale": {"track": {"uuid": track.uuid}}}, + ) + with temp_signal(signals.upload_import_status_updated) as handler: + tasks.process_upload(upload_id=duplicate.pk) - class MyException(Exception): - pass + duplicate.refresh_from_db() - mocker.patch("funkwhale_api.music.tasks._do_import", side_effect=MyException()) + assert duplicate.import_status == "skipped" + assert duplicate.import_details == { + "code": "already_imported_in_owned_libraries", + "duplicates": [str(existing.uuid)], + } - job = factories["music.FileImportJob"](audio_file__path=path, track_file=None) - - with pytest.raises(MyException): - tasks.import_job_run(import_job_id=job.pk) - - job.refresh_from_db() - - assert job.track_file is None - assert job.status == "errored" + handler.assert_called_once_with( + upload=duplicate, + old_status="pending", + new_status="skipped", + sender=None, + signal=signals.upload_import_status_updated, + ) -def test__do_import_calls_update_album_cover_if_no_cover(factories, mocker): - path = os.path.join(DATA_DIR, "test.ogg") +def test_upload_import_track_uuid(now, factories): + track = factories["music.Track"]() + upload = factories["music.Upload"]( + track=None, import_metadata={"funkwhale": {"track": {"uuid": track.uuid}}} + ) + + tasks.process_upload(upload_id=upload.pk) + + upload.refresh_from_db() + + assert upload.track == track + assert upload.import_status == "finished" + assert upload.import_date == now + + +def test_upload_import_skip_federation(now, factories, mocker): + outbox = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch") + track = factories["music.Track"]() + upload = factories["music.Upload"]( + track=None, + import_metadata={ + "funkwhale": { + "track": {"uuid": track.uuid}, + "config": {"dispatch_outbox": False}, + } + }, + ) + + tasks.process_upload(upload_id=upload.pk) + + outbox.assert_not_called() + + +def test_upload_import_skip_broadcast(now, factories, mocker): + group_send = mocker.patch("funkwhale_api.common.channels.group_send") + track = factories["music.Track"]() + upload = factories["music.Upload"]( + library__actor__local=True, + track=None, + import_metadata={ + "funkwhale": {"track": {"uuid": track.uuid}, "config": {"broadcast": False}} + }, + ) + + tasks.process_upload(upload_id=upload.pk) + + group_send.assert_not_called() + + +def test_upload_import_error(factories, now, temp_signal): + upload = factories["music.Upload"]( + import_metadata={"funkwhale": {"track": {"uuid": uuid.uuid4()}}} + ) + with temp_signal(signals.upload_import_status_updated) as handler: + tasks.process_upload(upload_id=upload.pk) + upload.refresh_from_db() + + assert upload.import_status == "errored" + assert upload.import_date == now + assert upload.import_details == {"error_code": "track_uuid_not_found"} + handler.assert_called_once_with( + upload=upload, + old_status="pending", + new_status="errored", + sender=None, + signal=signals.upload_import_status_updated, + ) + + +def test_upload_import_updates_cover_if_no_cover(factories, mocker, now): + mocked_update = mocker.patch("funkwhale_api.music.tasks.update_album_cover") album = factories["music.Album"](cover="") track = factories["music.Track"](album=album) - - mocker.patch( - "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path", - return_value=track, + upload = factories["music.Upload"]( + track=None, import_metadata={"funkwhale": {"track": {"uuid": track.uuid}}} ) - - mocked_update = mocker.patch("funkwhale_api.music.tasks.update_album_cover") - - job = factories["music.FileImportJob"](audio_file__path=path, track_file=None) - - tasks.import_job_run(import_job_id=job.pk) - - mocked_update.assert_called_once_with(album, track.files.first()) + tasks.process_upload(upload_id=upload.pk) + mocked_update.assert_called_once_with(album, source=None, cover_data=None) def test_update_album_cover_mbid(factories, mocker): album = factories["music.Album"](cover="") mocked_get = mocker.patch("funkwhale_api.music.models.Album.get_image") - tasks.update_album_cover(album=album, track_file=None) + tasks.update_album_cover(album=album) mocked_get.assert_called_once_with() def test_update_album_cover_file_data(factories, mocker): album = factories["music.Album"](cover="", mbid=None) - tf = factories["music.TrackFile"](track__album=album) mocked_get = mocker.patch("funkwhale_api.music.models.Album.get_image") - mocker.patch( - "funkwhale_api.music.metadata.Metadata.get_picture", - return_value={"hello": "world"}, - ) - tasks.update_album_cover(album=album, track_file=tf) - tf.get_metadata() + tasks.update_album_cover(album=album, cover_data={"hello": "world"}) mocked_get.assert_called_once_with(data={"hello": "world"}) @@ -254,12 +378,171 @@ def test_update_album_cover_file_cover_separate_file(ext, mimetype, factories, m with open(image_path, "rb") as f: image_content = f.read() album = factories["music.Album"](cover="", mbid=None) - tf = factories["music.TrackFile"](track__album=album, source="file://" + image_path) mocked_get = mocker.patch("funkwhale_api.music.models.Album.get_image") mocker.patch("funkwhale_api.music.metadata.Metadata.get_picture", return_value=None) - tasks.update_album_cover(album=album, track_file=tf) - tf.get_metadata() + tasks.update_album_cover(album=album, source="file://" + image_path) mocked_get.assert_called_once_with( data={"mimetype": mimetype, "content": image_content} ) + + +def test_federation_audio_track_to_metadata(now): + published = now + released = now.date() + payload = { + "type": "Track", + "id": "http://hello.track", + "musicbrainzId": str(uuid.uuid4()), + "name": "Black in back", + "position": 5, + "published": published.isoformat(), + "album": { + "published": published.isoformat(), + "type": "Album", + "id": "http://hello.album", + "name": "Purple album", + "musicbrainzId": str(uuid.uuid4()), + "released": released.isoformat(), + "artists": [ + { + "type": "Artist", + "published": published.isoformat(), + "id": "http://hello.artist", + "name": "John Smith", + "musicbrainzId": str(uuid.uuid4()), + } + ], + }, + "artists": [ + { + "published": published.isoformat(), + "type": "Artist", + "id": "http://hello.trackartist", + "name": "Bob Smith", + "musicbrainzId": str(uuid.uuid4()), + } + ], + } + serializer = federation_serializers.TrackSerializer(data=payload) + serializer.is_valid(raise_exception=True) + expected = { + "artist": payload["artists"][0]["name"], + "album": payload["album"]["name"], + "album_artist": payload["album"]["artists"][0]["name"], + "title": payload["name"], + "date": released, + "track_number": payload["position"], + # musicbrainz + "musicbrainz_albumid": payload["album"]["musicbrainzId"], + "musicbrainz_recordingid": payload["musicbrainzId"], + "musicbrainz_artistid": payload["artists"][0]["musicbrainzId"], + "musicbrainz_albumartistid": payload["album"]["artists"][0]["musicbrainzId"], + # federation + "fid": payload["id"], + "album_fid": payload["album"]["id"], + "artist_fid": payload["artists"][0]["id"], + "album_artist_fid": payload["album"]["artists"][0]["id"], + "fdate": serializer.validated_data["published"], + "artist_fdate": serializer.validated_data["artists"][0]["published"], + "album_artist_fdate": serializer.validated_data["album"]["artists"][0][ + "published" + ], + "album_fdate": serializer.validated_data["album"]["published"], + } + + result = tasks.federation_audio_track_to_metadata(serializer.validated_data) + assert result == expected + + # ensure we never forget to test a mandatory field + for k in metadata.ALL_FIELDS: + assert k in result + + +def test_scan_library_fetches_page_and_calls_scan_page(now, mocker, factories, r_mock): + scan = factories["music.LibraryScan"]() + collection_conf = { + "actor": scan.library.actor, + "id": scan.library.fid, + "page_size": 10, + "items": range(10), + "type": "Library", + "name": "hello", + } + collection = federation_serializers.PaginatedCollectionSerializer(collection_conf) + data = collection.data + data["followers"] = "https://followers.domain" + + scan_page = mocker.patch("funkwhale_api.music.tasks.scan_library_page.delay") + r_mock.get(collection_conf["id"], json=data) + tasks.start_library_scan(library_scan_id=scan.pk) + + scan_page.assert_called_once_with(library_scan_id=scan.pk, page_url=data["first"]) + scan.refresh_from_db() + + assert scan.status == "scanning" + assert scan.total_files == len(collection_conf["items"]) + assert scan.modification_date == now + + +def test_scan_page_fetches_page_and_creates_tracks(now, mocker, factories, r_mock): + scan_page = mocker.patch("funkwhale_api.music.tasks.scan_library_page.delay") + scan = factories["music.LibraryScan"](status="scanning", total_files=5) + uploads = [ + factories["music.Upload"].build( + fid="https://track.test/{}".format(i), + size=42, + bitrate=66, + duration=99, + library=scan.library, + ) + for i in range(5) + ] + + page_conf = { + "actor": scan.library.actor, + "id": scan.library.fid, + "page": Paginator(uploads, 3).page(1), + "item_serializer": federation_serializers.UploadSerializer, + } + page = federation_serializers.CollectionPageSerializer(page_conf) + r_mock.get(page.data["id"], json=page.data) + + tasks.scan_library_page(library_scan_id=scan.pk, page_url=page.data["id"]) + + scan.refresh_from_db() + lts = list(scan.library.uploads.all().order_by("-creation_date")) + + assert len(lts) == 3 + for upload in uploads[:3]: + scan.library.uploads.get(fid=upload.fid) + + assert scan.status == "scanning" + assert scan.processed_files == 3 + assert scan.modification_date == now + + scan_page.assert_called_once_with( + library_scan_id=scan.pk, page_url=page.data["next"] + ) + + +def test_scan_page_trigger_next_page_scan_skip_if_same(mocker, factories, r_mock): + patched_scan = mocker.patch("funkwhale_api.music.tasks.scan_library_page.delay") + scan = factories["music.LibraryScan"](status="scanning", total_files=5) + uploads = factories["music.Upload"].build_batch(size=5, library=scan.library) + page_conf = { + "actor": scan.library.actor, + "id": scan.library.fid, + "page": Paginator(uploads, 3).page(1), + "item_serializer": federation_serializers.UploadSerializer, + } + page = federation_serializers.CollectionPageSerializer(page_conf) + data = page.data + data["next"] = data["id"] + r_mock.get(page.data["id"], json=data) + + tasks.scan_library_page(library_scan_id=scan.pk, page_url=data["id"]) + patched_scan.assert_not_called() + scan.refresh_from_db() + + assert scan.status == "finished" diff --git a/api/tests/music/test_utils.py b/api/tests/music/test_utils.py index 4019e47b4..ecbfc49c9 100644 --- a/api/tests/music/test_utils.py +++ b/api/tests/music/test_utils.py @@ -9,7 +9,7 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__)) def test_guess_mimetype_try_using_extension(factories, mocker): mocker.patch("magic.from_buffer", return_value="audio/mpeg") - f = factories["music.TrackFile"].build(audio_file__filename="test.ogg") + f = factories["music.Upload"].build(audio_file__filename="test.ogg") assert utils.guess_mimetype(f.audio_file) == "audio/mpeg" @@ -17,7 +17,7 @@ def test_guess_mimetype_try_using_extension(factories, mocker): @pytest.mark.parametrize("wrong", ["application/octet-stream", "application/x-empty"]) def test_guess_mimetype_try_using_extension_if_fail(wrong, factories, mocker): mocker.patch("magic.from_buffer", return_value=wrong) - f = factories["music.TrackFile"].build(audio_file__filename="test.mp3") + f = factories["music.Upload"].build(audio_file__filename="test.mp3") assert utils.guess_mimetype(f.audio_file) == "audio/mpeg" diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py index aa04521cb..2f9d66e5b 100644 --- a/api/tests/music/test_views.py +++ b/api/tests/music/test_views.py @@ -1,26 +1,20 @@ import io +import os import pytest from django.urls import reverse from django.utils import timezone -from funkwhale_api.federation import actors -from funkwhale_api.music import serializers, views +from funkwhale_api.music import serializers, tasks, views +from funkwhale_api.federation import api_serializers as federation_api_serializers - -@pytest.mark.parametrize( - "view,permissions,operator", - [ - (views.ImportBatchViewSet, ["library", "upload"], "or"), - (views.ImportJobViewSet, ["library", "upload"], "or"), - ], -) -def test_permissions(assert_user_permission, view, permissions, operator): - assert_user_permission(view, permissions, operator) +DATA_DIR = os.path.dirname(os.path.abspath(__file__)) def test_artist_list_serializer(api_request, factories, logged_in_api_client): - track = factories["music.Track"]() + track = factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ).track artist = track.artist request = api_request.get("/") qs = artist.__class__.objects.with_albums() @@ -28,6 +22,9 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client): qs, many=True, context={"request": request} ) expected = {"count": 1, "next": None, "previous": None, "results": serializer.data} + for artist in serializer.data: + for album in artist["albums"]: + album["is_playable"] = True url = reverse("api:v1:artists-list") response = logged_in_api_client.get(url) @@ -36,7 +33,9 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client): def test_album_list_serializer(api_request, factories, logged_in_api_client): - track = factories["music.Track"]() + track = factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ).track album = track.album request = api_request.get("/") qs = album.__class__.objects.all() @@ -44,21 +43,26 @@ def test_album_list_serializer(api_request, factories, logged_in_api_client): qs, many=True, context={"request": request} ) expected = {"count": 1, "next": None, "previous": None, "results": serializer.data} + expected["results"][0]["is_playable"] = True + expected["results"][0]["tracks"][0]["is_playable"] = True url = reverse("api:v1:albums-list") response = logged_in_api_client.get(url) assert response.status_code == 200 - assert response.data == expected + assert response.data["results"][0] == expected["results"][0] def test_track_list_serializer(api_request, factories, logged_in_api_client): - track = factories["music.Track"]() + track = factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ).track request = api_request.get("/") qs = track.__class__.objects.all() serializer = serializers.TrackSerializer( qs, many=True, context={"request": request} ) expected = {"count": 1, "next": None, "previous": None, "results": serializer.data} + expected["results"][0]["is_playable"] = True url = reverse("api:v1:tracks-list") response = logged_in_api_client.get(url) @@ -67,13 +71,15 @@ def test_track_list_serializer(api_request, factories, logged_in_api_client): @pytest.mark.parametrize("param,expected", [("true", "full"), ("false", "empty")]) -def test_artist_view_filter_listenable(param, expected, factories, api_request): +def test_artist_view_filter_playable(param, expected, factories, api_request): artists = { "empty": factories["music.Artist"](), - "full": factories["music.TrackFile"]().track.artist, + "full": factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ).track.artist, } - request = api_request.get("/", {"listenable": param}) + request = api_request.get("/", {"playable": param}) view = views.ArtistViewSet() view.action_map = {"get": "list"} expected = [artists[expected]] @@ -84,13 +90,15 @@ def test_artist_view_filter_listenable(param, expected, factories, api_request): @pytest.mark.parametrize("param,expected", [("true", "full"), ("false", "empty")]) -def test_album_view_filter_listenable(param, expected, factories, api_request): +def test_album_view_filter_playable(param, expected, factories, api_request): artists = { "empty": factories["music.Album"](), - "full": factories["music.TrackFile"]().track.album, + "full": factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ).track.album, } - request = api_request.get("/", {"listenable": param}) + request = api_request.get("/", {"playable": param}) view = views.AlbumViewSet() view.action_map = {"get": "list"} expected = [artists[expected]] @@ -100,32 +108,36 @@ def test_album_view_filter_listenable(param, expected, factories, api_request): assert list(queryset) == expected -def test_can_serve_track_file_as_remote_library( - factories, authenticated_actor, api_client, settings, preferences +def test_can_serve_upload_as_remote_library( + factories, authenticated_actor, logged_in_api_client, settings, preferences ): preferences["common__api_authentication_required"] = True - library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + upload = factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ) + library_actor = upload.library.actor factories["federation.Follow"]( approved=True, actor=authenticated_actor, target=library_actor ) - track_file = factories["music.TrackFile"]() - response = api_client.get(track_file.path) + response = logged_in_api_client.get(upload.track.listen_url) assert response.status_code == 200 assert response["X-Accel-Redirect"] == "{}{}".format( - settings.PROTECT_FILES_PATH, track_file.audio_file.url + settings.PROTECT_FILES_PATH, upload.audio_file.url ) -def test_can_serve_track_file_as_remote_library_deny_not_following( +def test_can_serve_upload_as_remote_library_deny_not_following( factories, authenticated_actor, settings, api_client, preferences ): preferences["common__api_authentication_required"] = True - track_file = factories["music.TrackFile"]() - response = api_client.get(track_file.path) + upload = factories["music.Upload"]( + import_status="finished", library__privacy_level="instance" + ) + response = api_client.get(upload.track.listen_url) - assert response.status_code == 403 + assert response.status_code == 404 @pytest.mark.parametrize( @@ -146,10 +158,13 @@ def test_serve_file_in_place( settings.REVERSE_PROXY_TYPE = proxy settings.MUSIC_DIRECTORY_PATH = "/app/music" settings.MUSIC_DIRECTORY_SERVE_PATH = serve_path - tf = factories["music.TrackFile"]( - in_place=True, source="file:///app/music/hello/world.mp3" + upload = factories["music.Upload"]( + in_place=True, + import_status="finished", + source="file:///app/music/hello/world.mp3", + library__privacy_level="everyone", ) - response = api_client.get(tf.path) + response = api_client.get(upload.track.listen_url) assert response.status_code == 200 assert response[headers[proxy]] == expected @@ -198,9 +213,13 @@ def test_serve_file_media( settings.MUSIC_DIRECTORY_PATH = "/app/music" settings.MUSIC_DIRECTORY_SERVE_PATH = serve_path - tf = factories["music.TrackFile"]() - tf.__class__.objects.filter(pk=tf.pk).update(audio_file="tracks/hello/world.mp3") - response = api_client.get(tf.path) + upload = factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ) + upload.__class__.objects.filter(pk=upload.pk).update( + audio_file="tracks/hello/world.mp3" + ) + response = api_client.get(upload.track.listen_url) assert response.status_code == 200 assert response[headers[proxy]] == expected @@ -208,146 +227,256 @@ def test_serve_file_media( def test_can_proxy_remote_track(factories, settings, api_client, r_mock, preferences): preferences["common__api_authentication_required"] = False - track_file = factories["music.TrackFile"](federation=True) + url = "https://file.test" + upload = factories["music.Upload"]( + library__privacy_level="everyone", + audio_file="", + source=url, + import_status="finished", + ) - r_mock.get(track_file.library_track.audio_url, body=io.BytesIO(b"test")) - response = api_client.get(track_file.path) + r_mock.get(url, body=io.BytesIO(b"test")) + response = api_client.get(upload.track.listen_url) + upload.refresh_from_db() - library_track = track_file.library_track - library_track.refresh_from_db() assert response.status_code == 200 assert response["X-Accel-Redirect"] == "{}{}".format( - settings.PROTECT_FILES_PATH, library_track.audio_file.url + settings.PROTECT_FILES_PATH, upload.audio_file.url ) - assert library_track.audio_file.read() == b"test" + assert upload.audio_file.read() == b"test" def test_serve_updates_access_date(factories, settings, api_client, preferences): preferences["common__api_authentication_required"] = False - track_file = factories["music.TrackFile"]() + upload = factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ) now = timezone.now() - assert track_file.accessed_date is None + assert upload.accessed_date is None - response = api_client.get(track_file.path) - track_file.refresh_from_db() + response = api_client.get(upload.track.listen_url) + upload.refresh_from_db() assert response.status_code == 200 - assert track_file.accessed_date > now + assert upload.accessed_date > now -def test_can_list_import_jobs(factories, superuser_api_client): - job = factories["music.ImportJob"]() - url = reverse("api:v1:import-jobs-list") - response = superuser_api_client.get(url) +def test_listen_no_track(factories, logged_in_api_client): + url = reverse("api:v1:listen-detail", kwargs={"uuid": "noop"}) + response = logged_in_api_client.get(url) + + assert response.status_code == 404 + + +def test_listen_no_file(factories, logged_in_api_client): + track = factories["music.Track"]() + url = reverse("api:v1:listen-detail", kwargs={"uuid": track.uuid}) + response = logged_in_api_client.get(url) + + assert response.status_code == 404 + + +def test_listen_no_available_file(factories, logged_in_api_client): + upload = factories["music.Upload"]() + url = reverse("api:v1:listen-detail", kwargs={"uuid": upload.track.uuid}) + response = logged_in_api_client.get(url) + + assert response.status_code == 404 + + +def test_listen_correct_access(factories, logged_in_api_client): + logged_in_api_client.user.create_actor() + upload = factories["music.Upload"]( + library__actor=logged_in_api_client.user.actor, + library__privacy_level="me", + import_status="finished", + ) + url = reverse("api:v1:listen-detail", kwargs={"uuid": upload.track.uuid}) + response = logged_in_api_client.get(url) assert response.status_code == 200 - assert response.data["results"][0]["id"] == job.pk -def test_import_job_stats(factories, superuser_api_client): - factories["music.ImportJob"](status="pending") - factories["music.ImportJob"](status="errored") +def test_listen_explicit_file(factories, logged_in_api_client, mocker): + mocked_serve = mocker.spy(views, "handle_serve") + upload1 = factories["music.Upload"]( + library__privacy_level="everyone", import_status="finished" + ) + upload2 = factories["music.Upload"]( + library__privacy_level="everyone", track=upload1.track, import_status="finished" + ) + url = reverse("api:v1:listen-detail", kwargs={"uuid": upload2.track.uuid}) + response = logged_in_api_client.get(url, {"upload": upload2.uuid}) - url = reverse("api:v1:import-jobs-stats") - response = superuser_api_client.get(url) - expected = {"errored": 1, "pending": 1, "finished": 0, "skipped": 0, "count": 2} assert response.status_code == 200 - assert response.data == expected + mocked_serve.assert_called_once_with(upload2, user=logged_in_api_client.user) -def test_import_job_stats_filter(factories, superuser_api_client): - job1 = factories["music.ImportJob"](status="pending") - factories["music.ImportJob"](status="errored") +def test_user_can_create_library(factories, logged_in_api_client): + actor = logged_in_api_client.user.create_actor() + url = reverse("api:v1:libraries-list") + + response = logged_in_api_client.post( + url, {"name": "hello", "description": "world", "privacy_level": "me"} + ) + library = actor.libraries.first() + + assert response.status_code == 201 + + assert library.actor == actor + assert library.name == "hello" + assert library.description == "world" + assert library.privacy_level == "me" + assert library.fid == library.get_federation_id() + assert library.followers_url == library.fid + "/followers" + + +def test_user_can_list_their_library(factories, logged_in_api_client): + actor = logged_in_api_client.user.create_actor() + library = factories["music.Library"](actor=actor) + factories["music.Library"]() + + url = reverse("api:v1:libraries-list") + response = logged_in_api_client.get(url) - url = reverse("api:v1:import-jobs-stats") - response = superuser_api_client.get(url, {"batch": job1.batch.pk}) - expected = {"errored": 0, "pending": 1, "finished": 0, "skipped": 0, "count": 1} assert response.status_code == 200 - assert response.data == expected + assert response.data["count"] == 1 + assert response.data["results"][0]["uuid"] == str(library.uuid) -def test_import_job_run_via_api(factories, superuser_api_client, mocker): - run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay") - job1 = factories["music.ImportJob"](status="errored") - job2 = factories["music.ImportJob"](status="pending") +def test_user_cannot_delete_other_actors_library(factories, logged_in_api_client): + logged_in_api_client.user.create_actor() + library = factories["music.Library"](privacy_level="everyone") - url = reverse("api:v1:import-jobs-run") - response = superuser_api_client.post(url, {"jobs": [job2.pk, job1.pk]}) + url = reverse("api:v1:libraries-detail", kwargs={"uuid": library.uuid}) + response = logged_in_api_client.delete(url) - job1.refresh_from_db() - job2.refresh_from_db() - assert response.status_code == 200 - assert response.data == {"jobs": [job1.pk, job2.pk]} - assert job1.status == "pending" - assert job2.status == "pending" - - run.assert_any_call(import_job_id=job1.pk) - run.assert_any_call(import_job_id=job2.pk) + assert response.status_code == 404 -def test_import_batch_run_via_api(factories, superuser_api_client, mocker): - run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay") - - batch = factories["music.ImportBatch"]() - job1 = factories["music.ImportJob"](batch=batch, status="errored") - job2 = factories["music.ImportJob"](batch=batch, status="pending") - - url = reverse("api:v1:import-jobs-run") - response = superuser_api_client.post(url, {"batches": [batch.pk]}) - - job1.refresh_from_db() - job2.refresh_from_db() - assert response.status_code == 200 - assert job1.status == "pending" - assert job2.status == "pending" - - run.assert_any_call(import_job_id=job1.pk) - run.assert_any_call(import_job_id=job2.pk) - - -def test_import_batch_and_job_run_via_api(factories, superuser_api_client, mocker): - run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay") - - batch = factories["music.ImportBatch"]() - job1 = factories["music.ImportJob"](batch=batch, status="errored") - job2 = factories["music.ImportJob"](status="pending") - - url = reverse("api:v1:import-jobs-run") - response = superuser_api_client.post( - url, {"batches": [batch.pk], "jobs": [job2.pk]} +def test_library_delete_via_api_triggers_outbox(factories, mocker): + dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch") + library = factories["music.Library"]() + view = views.LibraryViewSet() + view.perform_destroy(library) + dispatch.assert_called_once_with( + {"type": "Delete", "object": {"type": "Library"}}, context={"library": library} ) - job1.refresh_from_db() - job2.refresh_from_db() + +def test_user_cannot_get_other_actors_uploads(factories, logged_in_api_client): + logged_in_api_client.user.create_actor() + upload = factories["music.Upload"]() + + url = reverse("api:v1:uploads-detail", kwargs={"uuid": upload.uuid}) + response = logged_in_api_client.get(url) + + assert response.status_code == 404 + + +def test_user_cannot_delete_other_actors_uploads(factories, logged_in_api_client): + logged_in_api_client.user.create_actor() + upload = factories["music.Upload"]() + + url = reverse("api:v1:uploads-detail", kwargs={"uuid": upload.uuid}) + response = logged_in_api_client.delete(url) + + assert response.status_code == 404 + + +def test_upload_delete_via_api_triggers_outbox(factories, mocker): + dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch") + upload = factories["music.Upload"]() + view = views.UploadViewSet() + view.perform_destroy(upload) + dispatch.assert_called_once_with( + {"type": "Delete", "object": {"type": "Audio"}}, context={"uploads": [upload]} + ) + + +def test_user_cannot_list_other_actors_uploads(factories, logged_in_api_client): + logged_in_api_client.user.create_actor() + factories["music.Upload"]() + + url = reverse("api:v1:uploads-list") + response = logged_in_api_client.get(url) + assert response.status_code == 200 - assert job1.status == "pending" - assert job2.status == "pending" - - run.assert_any_call(import_job_id=job1.pk) - run.assert_any_call(import_job_id=job2.pk) - - -def test_import_job_viewset_get_queryset_upload_filters_user( - factories, logged_in_api_client -): - logged_in_api_client.user.permission_upload = True - logged_in_api_client.user.save() - - factories["music.ImportJob"]() - url = reverse("api:v1:import-jobs-list") - response = logged_in_api_client.get(url) - assert response.data["count"] == 0 -def test_import_batch_viewset_get_queryset_upload_filters_user( - factories, logged_in_api_client -): - logged_in_api_client.user.permission_upload = True - logged_in_api_client.user.save() +def test_user_can_create_upload(logged_in_api_client, factories, mocker, audio_file): + library = factories["music.Library"](actor__user=logged_in_api_client.user) + url = reverse("api:v1:uploads-list") + m = mocker.patch("funkwhale_api.common.utils.on_commit") + + response = logged_in_api_client.post( + url, + { + "audio_file": audio_file, + "source": "upload://test", + "import_reference": "test", + "library": library.uuid, + }, + ) + + assert response.status_code == 201 + + upload = library.uploads.latest("id") + + audio_file.seek(0) + assert upload.audio_file.read() == audio_file.read() + assert upload.source == "upload://test" + assert upload.import_reference == "test" + assert upload.track is None + m.assert_called_once_with(tasks.process_upload.delay, upload_id=upload.pk) + + +def test_user_can_list_own_library_follows(factories, logged_in_api_client): + actor = logged_in_api_client.user.create_actor() + library = factories["music.Library"](actor=actor) + another_library = factories["music.Library"](actor=actor) + follow = factories["federation.LibraryFollow"](target=library) + factories["federation.LibraryFollow"](target=another_library) + + url = reverse("api:v1:libraries-follows", kwargs={"uuid": library.uuid}) - factories["music.ImportBatch"]() - url = reverse("api:v1:import-batches-list") response = logged_in_api_client.get(url) - assert response.data["count"] == 0 + assert response.data == { + "count": 1, + "next": None, + "previous": None, + "results": [federation_api_serializers.LibraryFollowSerializer(follow).data], + } + + +@pytest.mark.parametrize("entity", ["artist", "album", "track"]) +def test_can_get_libraries_for_music_entities( + factories, api_client, entity, preferences +): + preferences["common__api_authentication_required"] = False + upload = factories["music.Upload"](playable=True) + # another private library that should not appear + factories["music.Upload"]( + import_status="finished", library__privacy_level="me", track=upload.track + ).library + library = upload.library + data = { + "artist": upload.track.artist, + "album": upload.track.album, + "track": upload.track, + } + + url = reverse("api:v1:{}s-libraries".format(entity), kwargs={"pk": data[entity].pk}) + + response = api_client.get(url) + expected = federation_api_serializers.LibrarySerializer(library).data + + assert response.status_code == 200 + assert response.data == { + "count": 1, + "next": None, + "previous": None, + "results": [expected], + } diff --git a/api/tests/playlists/test_models.py b/api/tests/playlists/test_models.py index 25c40d557..46c14d11c 100644 --- a/api/tests/playlists/test_models.py +++ b/api/tests/playlists/test_models.py @@ -122,3 +122,38 @@ def test_insert_many_honor_max_tracks(preferences, factories): track = factories["music.Track"]() with pytest.raises(exceptions.ValidationError): playlist.insert_many([track, track, track]) + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", False), ("everyone", True)] +) +def test_playlist_track_playable_by_anonymous(privacy_level, expected, factories): + plt = factories["playlists.PlaylistTrack"]() + track = plt.track + factories["music.Upload"]( + track=track, library__privacy_level=privacy_level, import_status="finished" + ) + queryset = plt.__class__.objects.playable_by(None).annotate_playable_by_actor(None) + match = plt in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected + + +@pytest.mark.parametrize( + "privacy_level,expected", [("me", False), ("instance", False), ("everyone", True)] +) +def test_playlist_playable_by_anonymous(privacy_level, expected, factories): + plt = factories["playlists.PlaylistTrack"]() + playlist = plt.playlist + track = plt.track + factories["music.Upload"]( + track=track, library__privacy_level=privacy_level, import_status="finished" + ) + queryset = playlist.__class__.objects.playable_by(None).annotate_playable_by_actor( + None + ) + match = playlist in list(queryset) + assert match is expected + if expected: + assert bool(queryset.first().is_playable_by_actor) is expected diff --git a/api/tests/playlists/test_serializers.py b/api/tests/playlists/test_serializers.py index 79765a24b..0afc927ad 100644 --- a/api/tests/playlists/test_serializers.py +++ b/api/tests/playlists/test_serializers.py @@ -93,9 +93,9 @@ def test_playlist_serializer_include_covers(factories, api_request): def test_playlist_serializer_include_duration(factories, api_request): playlist = factories["playlists.Playlist"]() - tf1 = factories["music.TrackFile"](duration=15) - tf2 = factories["music.TrackFile"](duration=30) - playlist.insert_many([tf1.track, tf2.track]) + upload1 = factories["music.Upload"](duration=15) + upload2 = factories["music.Upload"](duration=30) + playlist.insert_many([upload1.track, upload2.track]) qs = playlist.__class__.objects.with_duration().with_tracks_count() serializer = serializers.PlaylistSerializer(qs.get()) diff --git a/api/tests/playlists/test_views.py b/api/tests/playlists/test_views.py index e7b47c7a2..1256347f3 100644 --- a/api/tests/playlists/test_views.py +++ b/api/tests/playlists/test_views.py @@ -25,6 +25,16 @@ def test_serializer_includes_tracks_count(factories, logged_in_api_client): assert response.data["tracks_count"] == 1 +def test_serializer_includes_is_playable(factories, logged_in_api_client): + playlist = factories["playlists.Playlist"]() + factories["playlists.PlaylistTrack"](playlist=playlist) + + url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk}) + response = logged_in_api_client.get(url) + + assert response.data["is_playable"] is False + + def test_playlist_inherits_user_privacy(logged_in_api_client): url = reverse("api:v1:playlists-list") user = logged_in_api_client.user @@ -135,7 +145,7 @@ def test_can_list_tracks_from_playlist(level, factories, logged_in_api_client): url = reverse("api:v1:playlists-tracks", kwargs={"pk": plt.playlist.pk}) response = logged_in_api_client.get(url) serialized_plt = serializers.PlaylistTrackSerializer(plt).data - + serialized_plt["track"]["is_playable"] = False assert response.data["count"] == 1 assert response.data["results"][0] == serialized_plt diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py index e218ced90..7e8f260d0 100644 --- a/api/tests/radios/test_radios.py +++ b/api/tests/radios/test_radios.py @@ -48,7 +48,7 @@ def test_can_pick_by_weight(): def test_can_get_choices_for_favorites_radio(factories): - files = factories["music.TrackFile"].create_batch(10) + files = factories["music.Upload"].create_batch(10) tracks = [f.track for f in files] user = factories["users.User"]() for i in range(5): @@ -69,14 +69,14 @@ def test_can_get_choices_for_favorites_radio(factories): def test_can_get_choices_for_custom_radio(factories): artist = factories["music.Artist"]() - files = factories["music.TrackFile"].create_batch(5, track__artist=artist) + files = factories["music.Upload"].create_batch(5, track__artist=artist) tracks = [f.track for f in files] - factories["music.TrackFile"].create_batch(5) + factories["music.Upload"].create_batch(5) session = factories["radios.CustomRadioSession"]( custom_radio__config=[{"type": "artist", "ids": [artist.pk]}] ) - choices = session.radio.get_choices() + choices = session.radio.get_choices(filter_playable=False) expected = [t.pk for t in tracks] assert list(choices.values_list("id", flat=True)) == expected @@ -94,35 +94,35 @@ def test_cannot_start_custom_radio_if_not_owner_or_not_public(factories): assert message in serializer.errors["non_field_errors"] -def test_can_start_custom_radio_from_api(logged_in_client, factories): +def test_can_start_custom_radio_from_api(logged_in_api_client, factories): artist = factories["music.Artist"]() radio = factories["radios.Radio"]( - config=[{"type": "artist", "ids": [artist.pk]}], user=logged_in_client.user + config=[{"type": "artist", "ids": [artist.pk]}], user=logged_in_api_client.user ) url = reverse("api:v1:radios:sessions-list") - response = logged_in_client.post( + response = logged_in_api_client.post( url, {"radio_type": "custom", "custom_radio": radio.pk} ) assert response.status_code == 201 session = radio.sessions.latest("id") assert session.radio_type == "custom" - assert session.user == logged_in_client.user + assert session.user == logged_in_api_client.user def test_can_use_radio_session_to_filter_choices(factories): - factories["music.TrackFile"].create_batch(30) + factories["music.Upload"].create_batch(10) user = factories["users.User"]() radio = radios.RandomRadio() session = radio.start_session(user) - for i in range(30): - radio.pick() + for i in range(10): + radio.pick(filter_playable=False) - # ensure 30 differents tracks have been suggested + # ensure 10 differents tracks have been suggested tracks_id = [ session_track.track.pk for session_track in session.session_tracks.all() ] - assert len(set(tracks_id)) == 30 + assert len(set(tracks_id)) == 10 def test_can_restore_radio_from_previous_session(factories): @@ -134,30 +134,34 @@ def test_can_restore_radio_from_previous_session(factories): assert radio.session == restarted_radio.session -def test_can_start_radio_for_logged_in_user(logged_in_client): +def test_can_start_radio_for_logged_in_user(logged_in_api_client): url = reverse("api:v1:radios:sessions-list") - logged_in_client.post(url, {"radio_type": "random"}) + logged_in_api_client.post(url, {"radio_type": "random"}) session = models.RadioSession.objects.latest("id") assert session.radio_type == "random" - assert session.user == logged_in_client.user + assert session.user == logged_in_api_client.user -def test_can_get_track_for_session_from_api(factories, logged_in_client): - files = factories["music.TrackFile"].create_batch(1) - tracks = [f.track for f in files] +def test_can_get_track_for_session_from_api(factories, logged_in_api_client): + actor = logged_in_api_client.user.create_actor() + track = factories["music.Upload"]( + library__actor=actor, import_status="finished" + ).track url = reverse("api:v1:radios:sessions-list") - response = logged_in_client.post(url, {"radio_type": "random"}) + response = logged_in_api_client.post(url, {"radio_type": "random"}) session = models.RadioSession.objects.latest("id") url = reverse("api:v1:radios:tracks-list") - response = logged_in_client.post(url, {"session": session.pk}) + response = logged_in_api_client.post(url, {"session": session.pk}) data = json.loads(response.content.decode("utf-8")) - assert data["track"]["id"] == tracks[0].id + assert data["track"]["id"] == track.pk assert data["position"] == 1 - next_track = factories["music.TrackFile"]().track - response = logged_in_client.post(url, {"session": session.pk}) + next_track = factories["music.Upload"]( + library__actor=actor, import_status="finished" + ).track + response = logged_in_api_client.post(url, {"session": session.pk}) data = json.loads(response.content.decode("utf-8")) assert data["track"]["id"] == next_track.id @@ -180,29 +184,29 @@ def test_related_object_radio_validate_related_object(factories): def test_can_start_artist_radio(factories): user = factories["users.User"]() artist = factories["music.Artist"]() - factories["music.TrackFile"].create_batch(5) - good_files = factories["music.TrackFile"].create_batch(5, track__artist=artist) + factories["music.Upload"].create_batch(5) + good_files = factories["music.Upload"].create_batch(5, track__artist=artist) good_tracks = [f.track for f in good_files] radio = radios.ArtistRadio() session = radio.start_session(user, related_object=artist) assert session.radio_type == "artist" for i in range(5): - assert radio.pick() in good_tracks + assert radio.pick(filter_playable=False) in good_tracks def test_can_start_tag_radio(factories): user = factories["users.User"]() tag = factories["taggit.Tag"]() - factories["music.TrackFile"].create_batch(5) - good_files = factories["music.TrackFile"].create_batch(5, track__tags=[tag]) + factories["music.Upload"].create_batch(5) + good_files = factories["music.Upload"].create_batch(5, track__tags=[tag]) good_tracks = [f.track for f in good_files] radio = radios.TagRadio() session = radio.start_session(user, related_object=tag) assert session.radio_type == "tag" for i in range(5): - assert radio.pick() in good_tracks + assert radio.pick(filter_playable=False) in good_tracks def test_can_start_artist_radio_from_api(logged_in_api_client, preferences, factories): @@ -223,13 +227,13 @@ def test_can_start_artist_radio_from_api(logged_in_api_client, preferences, fact def test_can_start_less_listened_radio(factories): user = factories["users.User"]() - wrong_files = factories["music.TrackFile"].create_batch(5) + wrong_files = factories["music.Upload"].create_batch(5) for f in wrong_files: factories["history.Listening"](track=f.track, user=user) - good_files = factories["music.TrackFile"].create_batch(5) + good_files = factories["music.Upload"].create_batch(5) good_tracks = [f.track for f in good_files] radio = radios.LessListenedRadio() radio.start_session(user) for i in range(5): - assert radio.pick() in good_tracks + assert radio.pick(filter_playable=False) in good_tracks diff --git a/api/tests/requests/__init__.py b/api/tests/requests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/tests/requests/test_models.py b/api/tests/requests/test_models.py deleted file mode 100644 index 3ac8a5342..000000000 --- a/api/tests/requests/test_models.py +++ /dev/null @@ -1,18 +0,0 @@ -def test_can_bind_import_batch_to_request(factories): - request = factories["requests.ImportRequest"]() - - assert request.status == "pending" - - # when we create the import, we consider the request as accepted - batch = factories["music.ImportBatch"](import_request=request) - request.refresh_from_db() - - assert request.status == "accepted" - - # now, the batch is finished, therefore the request status should be - # imported - batch.status = "finished" - batch.save(update_fields=["status"]) - request.refresh_from_db() - - assert request.status == "imported" diff --git a/api/tests/requests/test_views.py b/api/tests/requests/test_views.py deleted file mode 100644 index 0d6433672..000000000 --- a/api/tests/requests/test_views.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.urls import reverse - - -def test_request_viewset_requires_auth(db, api_client): - url = reverse("api:v1:requests:import-requests-list") - response = api_client.get(url) - assert response.status_code == 401 - - -def test_user_can_create_request(logged_in_api_client): - url = reverse("api:v1:requests:import-requests-list") - user = logged_in_api_client.user - data = { - "artist_name": "System of a Down", - "albums": "All please!", - "comment": "Please, they rock!", - } - response = logged_in_api_client.post(url, data) - - assert response.status_code == 201 - - ir = user.import_requests.latest("id") - assert ir.status == "pending" - assert ir.creation_date is not None - for field, value in data.items(): - assert getattr(ir, field) == value diff --git a/api/tests/subsonic/test_serializers.py b/api/tests/subsonic/test_serializers.py index 6fdf02e2d..85cb65fa7 100644 --- a/api/tests/subsonic/test_serializers.py +++ b/api/tests/subsonic/test_serializers.py @@ -6,6 +6,7 @@ def test_get_artists_serializer(factories): artist1 = factories["music.Artist"](name="eliot") artist2 = factories["music.Artist"](name="Ellena") artist3 = factories["music.Artist"](name="Rilay") + artist4 = factories["music.Artist"](name="") # Shouldn't be serialised factories["music.Album"].create_batch(size=3, artist=artist1) factories["music.Album"].create_batch(size=2, artist=artist2) @@ -28,7 +29,7 @@ def test_get_artists_serializer(factories): } queryset = artist1.__class__.objects.filter( - pk__in=[artist1.pk, artist2.pk, artist3.pk] + pk__in=[artist1.pk, artist2.pk, artist3.pk, artist4.pk] ) assert serializers.GetArtistsSerializer(queryset).data == expected @@ -64,7 +65,7 @@ def test_get_album_serializer(factories): artist = factories["music.Artist"]() album = factories["music.Album"](artist=artist) track = factories["music.Track"](album=album) - tf = factories["music.TrackFile"](track=track, bitrate=42000, duration=43, size=44) + upload = factories["music.Upload"](track=track, bitrate=42000, duration=43, size=44) expected = { "id": album.pk, @@ -85,8 +86,8 @@ def test_get_album_serializer(factories): "artist": artist.name, "track": track.position, "year": track.album.release_date.year, - "contentType": tf.mimetype, - "suffix": tf.extension or "", + "contentType": upload.mimetype, + "suffix": upload.extension or "", "bitrate": 42, "duration": 43, "size": 44, @@ -105,9 +106,9 @@ def test_starred_tracks2_serializer(factories): artist = factories["music.Artist"]() album = factories["music.Album"](artist=artist) track = factories["music.Track"](album=album) - tf = factories["music.TrackFile"](track=track) + upload = factories["music.Upload"](track=track) favorite = factories["favorites.TrackFavorite"](track=track) - expected = [serializers.get_track_data(album, track, tf)] + expected = [serializers.get_track_data(album, track, upload)] expected[0]["starred"] = favorite.creation_date data = serializers.get_starred_tracks_data([favorite]) assert data == expected @@ -146,7 +147,7 @@ def test_playlist_serializer(factories): def test_playlist_detail_serializer(factories): plt = factories["playlists.PlaylistTrack"]() - tf = factories["music.TrackFile"](track=plt.track) + upload = factories["music.Upload"](track=plt.track) playlist = plt.playlist qs = music_models.Album.objects.with_tracks_count().order_by("pk") expected = { @@ -157,7 +158,7 @@ def test_playlist_detail_serializer(factories): "songCount": 1, "duration": 0, "created": playlist.creation_date, - "entry": [serializers.get_track_data(plt.track.album, plt.track, tf)], + "entry": [serializers.get_track_data(plt.track.album, plt.track, upload)], } qs = playlist.__class__.objects.with_tracks_count() data = serializers.get_playlist_detail_data(qs.first()) @@ -166,7 +167,7 @@ def test_playlist_detail_serializer(factories): def test_directory_serializer_artist(factories): track = factories["music.Track"]() - tf = factories["music.TrackFile"](track=track, bitrate=42000, duration=43, size=44) + upload = factories["music.Upload"](track=track, bitrate=42000, duration=43, size=44) album = track.album artist = track.artist @@ -183,8 +184,8 @@ def test_directory_serializer_artist(factories): "artist": artist.name, "track": track.position, "year": track.album.release_date.year, - "contentType": tf.mimetype, - "suffix": tf.extension or "", + "contentType": upload.mimetype, + "suffix": upload.extension or "", "bitrate": 42, "duration": 43, "size": 44, @@ -201,8 +202,8 @@ def test_directory_serializer_artist(factories): def test_scrobble_serializer(factories): - tf = factories["music.TrackFile"]() - track = tf.track + upload = factories["music.Upload"]() + track = upload.track user = factories["users.User"]() payload = {"id": track.pk, "submission": True} serializer = serializers.ScrobbleSerializer(data=payload, context={"user": user}) diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index b7431efab..94cbd8c16 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -74,10 +74,13 @@ def test_ping(f, db, api_client): @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_artists(f, db, logged_in_api_client, factories): +def test_get_artists( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-artists") assert url.endswith("getArtists") is True - factories["music.Artist"].create_batch(size=10) + factories["music.Artist"].create_batch(size=3, playable=True) + playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by") expected = { "artists": serializers.GetArtistsSerializer( music_models.Artist.objects.all() @@ -87,26 +90,46 @@ def test_get_artists(f, db, logged_in_api_client, factories): assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with(music_models.Artist.objects.all(), None) @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_artist(f, db, logged_in_api_client, factories): +def test_get_artist( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-artist") assert url.endswith("getArtist") is True - artist = factories["music.Artist"]() - factories["music.Album"].create_batch(size=3, artist=artist) + artist = factories["music.Artist"](playable=True) + factories["music.Album"].create_batch(size=3, artist=artist, playable=True) + playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by") + expected = {"artist": serializers.GetArtistSerializer(artist).data} response = logged_in_api_client.get(url, {"id": artist.pk}) assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with(music_models.Artist.objects.all(), None) + + +@pytest.mark.parametrize("f", ["xml", "json"]) +def test_get_invalid_artist(f, db, logged_in_api_client, factories): + url = reverse("api:subsonic-get-artist") + assert url.endswith("getArtist") is True + expected = {"error": {"code": 0, "message": 'For input string "asdf"'}} + response = logged_in_api_client.get(url, {"id": "asdf"}) + + assert response.status_code == 200 + assert response.data == expected @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_artist_info2(f, db, logged_in_api_client, factories): +def test_get_artist_info2( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-artist-info2") assert url.endswith("getArtistInfo2") is True - artist = factories["music.Artist"]() + artist = factories["music.Artist"](playable=True) + playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by") expected = {"artist-info2": {}} response = logged_in_api_client.get(url, {"id": artist.pk}) @@ -114,34 +137,62 @@ def test_get_artist_info2(f, db, logged_in_api_client, factories): assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with(music_models.Artist.objects.all(), None) + @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_album(f, db, logged_in_api_client, factories): +def test_get_album( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-album") assert url.endswith("getAlbum") is True artist = factories["music.Artist"]() album = factories["music.Album"](artist=artist) - factories["music.Track"].create_batch(size=3, album=album) + factories["music.Track"].create_batch(size=3, album=album, playable=True) + playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by") expected = {"album": serializers.GetAlbumSerializer(album).data} response = logged_in_api_client.get(url, {"f": f, "id": album.pk}) assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with( + music_models.Album.objects.select_related("artist"), None + ) + @pytest.mark.parametrize("f", ["xml", "json"]) -def test_stream(f, db, logged_in_api_client, factories, mocker): +def test_get_song( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): + url = reverse("api:subsonic-get-song") + assert url.endswith("getSong") is True + artist = factories["music.Artist"]() + album = factories["music.Album"](artist=artist) + track = factories["music.Track"](album=album, playable=True) + upload = factories["music.Upload"](track=track) + playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by") + response = logged_in_api_client.get(url, {"f": f, "id": track.pk}) + + assert response.status_code == 200 + assert response.data == { + "song": serializers.get_track_data(track.album, track, upload) + } + playable_by.assert_called_once_with(music_models.Track.objects.all(), None) + + +@pytest.mark.parametrize("f", ["xml", "json"]) +def test_stream(f, db, logged_in_api_client, factories, mocker, queryset_equal_queries): url = reverse("api:subsonic-stream") mocked_serve = mocker.spy(music_views, "handle_serve") assert url.endswith("stream") is True - artist = factories["music.Artist"]() - album = factories["music.Album"](artist=artist) - track = factories["music.Track"](album=album) - tf = factories["music.TrackFile"](track=track) - response = logged_in_api_client.get(url, {"f": f, "id": track.pk}) + upload = factories["music.Upload"](playable=True) + playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by") + response = logged_in_api_client.get(url, {"f": f, "id": upload.track.pk}) - mocked_serve.assert_called_once_with(track_file=tf) + mocked_serve.assert_called_once_with(upload=upload, user=logged_in_api_client.user) assert response.status_code == 200 + playable_by.assert_called_once_with(music_models.Track.objects.all(), None) @pytest.mark.parametrize("f", ["xml", "json"]) @@ -204,25 +255,30 @@ def test_get_starred(f, db, logged_in_api_client, factories): @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_album_list2(f, db, logged_in_api_client, factories): +def test_get_album_list2( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-album-list2") assert url.endswith("getAlbumList2") is True - album1 = factories["music.Album"]() - album2 = factories["music.Album"]() + album1 = factories["music.Album"](playable=True) + album2 = factories["music.Album"](playable=True) + factories["music.Album"]() + playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by") response = logged_in_api_client.get(url, {"f": f, "type": "newest"}) assert response.status_code == 200 assert response.data == { "albumList2": {"album": serializers.get_album_list2_data([album2, album1])} } + playable_by.assert_called_once() @pytest.mark.parametrize("f", ["xml", "json"]) def test_get_album_list2_pagination(f, db, logged_in_api_client, factories): url = reverse("api:subsonic-get-album-list2") assert url.endswith("getAlbumList2") is True - album1 = factories["music.Album"]() - factories["music.Album"]() + album1 = factories["music.Album"](playable=True) + factories["music.Album"](playable=True) response = logged_in_api_client.get( url, {"f": f, "type": "newest", "size": 1, "offset": 1} ) @@ -237,12 +293,15 @@ def test_get_album_list2_pagination(f, db, logged_in_api_client, factories): def test_search3(f, db, logged_in_api_client, factories): url = reverse("api:subsonic-search3") assert url.endswith("search3") is True - artist = factories["music.Artist"](name="testvalue") + artist = factories["music.Artist"](name="testvalue", playable=True) factories["music.Artist"](name="nope") - album = factories["music.Album"](title="testvalue") + factories["music.Artist"](name="nope2", playable=True) + album = factories["music.Album"](title="testvalue", playable=True) factories["music.Album"](title="nope") - track = factories["music.Track"](title="testvalue") + factories["music.Album"](title="nope2", playable=True) + track = factories["music.Track"](title="testvalue", playable=True) factories["music.Track"](title="nope") + factories["music.Track"](title="nope2", playable=True) response = logged_in_api_client.get(url, {"f": f, "query": "testval"}) @@ -358,20 +417,25 @@ def test_get_music_folders(f, db, logged_in_api_client, factories): @pytest.mark.parametrize("f", ["xml", "json"]) -def test_get_indexes(f, db, logged_in_api_client, factories): +def test_get_indexes( + f, db, logged_in_api_client, factories, mocker, queryset_equal_queries +): url = reverse("api:subsonic-get-indexes") assert url.endswith("getIndexes") is True - factories["music.Artist"].create_batch(size=10) + factories["music.Artist"].create_batch(size=3, playable=True) expected = { "indexes": serializers.GetArtistsSerializer( music_models.Artist.objects.all() ).data } + playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by") response = logged_in_api_client.get(url) assert response.status_code == 200 assert response.data == expected + playable_by.assert_called_once_with(music_models.Artist.objects.all(), None) + def test_get_cover_art_album(factories, logged_in_api_client): url = reverse("api:subsonic-get-cover-art") @@ -387,8 +451,8 @@ def test_get_cover_art_album(factories, logged_in_api_client): def test_scrobble(factories, logged_in_api_client): - tf = factories["music.TrackFile"]() - track = tf.track + upload = factories["music.Upload"]() + track = upload.track url = reverse("api:subsonic-scrobble") assert url.endswith("scrobble") is True response = logged_in_api_client.get(url, {"id": track.pk, "submission": True}) diff --git a/api/tests/test_downloader.py b/api/tests/test_downloader.py deleted file mode 100644 index 0a4134393..000000000 --- a/api/tests/test_downloader.py +++ /dev/null @@ -1,11 +0,0 @@ -import os - -from funkwhale_api import downloader - - -def test_can_download_audio_from_youtube_url_to_vorbis(tmpdir): - data = downloader.download( - "https://www.youtube.com/watch?v=tPEE9ZwTmy0", target_directory=tmpdir - ) - assert data["audio_file_path"] == os.path.join(tmpdir, "tPEE9ZwTmy0.ogg") - assert os.path.exists(data["audio_file_path"]) diff --git a/api/tests/test_import_audio_file.py b/api/tests/test_import_audio_file.py index f63e69b63..ce6aebbc3 100644 --- a/api/tests/test_import_audio_file.py +++ b/api/tests/test_import_audio_file.py @@ -1,196 +1,162 @@ -import datetime import os import pytest from django.core.management import call_command from django.core.management.base import CommandError -from funkwhale_api.providers.audiofile import tasks -from funkwhale_api.music.models import ImportJob DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files") -def test_can_create_track_from_file_metadata_no_mbid(db, mocker): - metadata = { - "artist": ["Test artist"], - "album": ["Test album"], - "title": ["Test track"], - "TRACKNUMBER": ["4"], - "date": ["2012-08-15"], - } - mocker.patch("mutagen.File", return_value=metadata) - mocker.patch( - "funkwhale_api.music.metadata.Metadata.get_file_type", return_value="OggVorbis" - ) - track = tasks.import_track_data_from_path(os.path.join(DATA_DIR, "dummy_file.ogg")) - - assert track.title == metadata["title"][0] - assert track.mbid is None - assert track.position == 4 - assert track.album.title == metadata["album"][0] - assert track.album.mbid is None - assert track.album.release_date == datetime.date(2012, 8, 15) - assert track.artist.name == metadata["artist"][0] - assert track.artist.mbid is None - - -def test_can_create_track_from_file_metadata_mbid(factories, mocker): - album = factories["music.Album"]() - artist = factories["music.Artist"]() - mocker.patch( - "funkwhale_api.music.models.Album.get_or_create_from_api", - return_value=(album, True), - ) - - album_data = { - "release": { - "id": album.mbid, - "medium-list": [ - { - "track-list": [ - { - "id": "03baca8b-855a-3c05-8f3d-d3235287d84d", - "position": "4", - "number": "4", - "recording": { - "id": "2109e376-132b-40ad-b993-2bb6812e19d4", - "title": "Teen Age Riot", - "artist-credit": [ - {"artist": {"id": artist.mbid, "name": artist.name}} - ], - }, - } - ], - "track-count": 1, - } - ], - } - } - mocker.patch("funkwhale_api.musicbrainz.api.releases.get", return_value=album_data) - track_data = album_data["release"]["medium-list"][0]["track-list"][0] - metadata = { - "musicbrainz_albumid": [album.mbid], - "musicbrainz_trackid": [track_data["recording"]["id"]], - } - mocker.patch("mutagen.File", return_value=metadata) - mocker.patch( - "funkwhale_api.music.metadata.Metadata.get_file_type", return_value="OggVorbis" - ) - track = tasks.import_track_data_from_path(os.path.join(DATA_DIR, "dummy_file.ogg")) - - assert track.title == track_data["recording"]["title"] - assert track.mbid == track_data["recording"]["id"] - assert track.position == 4 - assert track.album == album - assert track.artist == artist - - -def test_management_command_requires_a_valid_username(factories, mocker): +def test_management_command_requires_a_valid_library_id(factories): path = os.path.join(DATA_DIR, "dummy_file.ogg") - factories["users.User"](username="me") - mocker.patch( - "funkwhale_api.providers.audiofile.management.commands.import_files.Command.do_import", # noqa - return_value=(mocker.MagicMock(), []), - ) - with pytest.raises(CommandError): - call_command("import_files", path, username="not_me", interactive=False) - call_command("import_files", path, username="me", interactive=False) + + with pytest.raises(CommandError) as e: + call_command("import_files", "wrong_id", path, interactive=False) + assert "Invalid library id" in str(e) def test_in_place_import_only_from_music_dir(factories, settings): - factories["users.User"](username="me") + library = factories["music.Library"](actor__local=True) settings.MUSIC_DIRECTORY_PATH = "/nope" path = os.path.join(DATA_DIR, "dummy_file.ogg") - with pytest.raises(CommandError): + with pytest.raises(CommandError) as e: call_command( - "import_files", path, in_place=True, username="me", interactive=False + "import_files", str(library.uuid), path, in_place=True, interactive=False ) + assert "Importing in-place only works if importing" in str(e) + def test_import_with_multiple_argument(factories, mocker): - factories["users.User"](username="me") + library = factories["music.Library"](actor__local=True) path1 = os.path.join(DATA_DIR, "dummy_file.ogg") path2 = os.path.join(DATA_DIR, "utf8-éà◌.ogg") mocked_filter = mocker.patch( - "funkwhale_api.providers.audiofile.management.commands.import_files.Command.filter_matching", + "funkwhale_api.music.management.commands.import_files.Command.filter_matching", return_value=({"new": [], "skipped": []}), ) - call_command("import_files", path1, path2, username="me", interactive=False) - mocked_filter.assert_called_once_with([path1, path2]) + call_command("import_files", str(library.uuid), path1, path2, interactive=False) + mocked_filter.assert_called_once_with([path1, path2], library) + + +@pytest.mark.parametrize( + "path", + [os.path.join(DATA_DIR, "dummy_file.ogg"), os.path.join(DATA_DIR, "utf8-éà◌.ogg")], +) +def test_import_files_stores_proper_data(factories, mocker, now, path): + mocked_process = mocker.patch("funkwhale_api.music.tasks.process_upload") + library = factories["music.Library"](actor__local=True) + call_command( + "import_files", str(library.uuid), path, async_=False, interactive=False + ) + upload = library.uploads.last() + assert upload.import_reference == "cli-{}".format(now.isoformat()) + assert upload.import_status == "pending" + assert upload.source == "file://{}".format(path) + assert upload.import_metadata == { + "funkwhale": { + "config": {"replace": False, "dispatch_outbox": False, "broadcast": False} + } + } + + mocked_process.assert_called_once_with(upload_id=upload.pk) + + +def test_import_with_outbox_flag(factories, mocker): + library = factories["music.Library"](actor__local=True) + path = os.path.join(DATA_DIR, "dummy_file.ogg") + mocked_process = mocker.patch("funkwhale_api.music.tasks.process_upload") + call_command( + "import_files", str(library.uuid), path, outbox=True, interactive=False + ) + upload = library.uploads.last() + + assert upload.import_metadata["funkwhale"]["config"]["dispatch_outbox"] is True + + mocked_process.assert_called_once_with(upload_id=upload.pk) + + +def test_import_with_broadcast_flag(factories, mocker): + library = factories["music.Library"](actor__local=True) + path = os.path.join(DATA_DIR, "dummy_file.ogg") + mocked_process = mocker.patch("funkwhale_api.music.tasks.process_upload") + call_command( + "import_files", str(library.uuid), path, broadcast=True, interactive=False + ) + upload = library.uploads.last() + + assert upload.import_metadata["funkwhale"]["config"]["broadcast"] is True + + mocked_process.assert_called_once_with(upload_id=upload.pk) def test_import_with_replace_flag(factories, mocker): - factories["users.User"](username="me") + library = factories["music.Library"](actor__local=True) path = os.path.join(DATA_DIR, "dummy_file.ogg") - mocked_job_run = mocker.patch("funkwhale_api.music.tasks.import_job_run") - call_command("import_files", path, username="me", replace=True, interactive=False) - created_job = ImportJob.objects.latest("id") - - assert created_job.replace_if_duplicate is True - mocked_job_run.assert_called_once_with( - import_job_id=created_job.id, use_acoustid=False + mocked_process = mocker.patch("funkwhale_api.music.tasks.process_upload") + call_command( + "import_files", str(library.uuid), path, replace=True, interactive=False ) + upload = library.uploads.last() + + assert upload.import_metadata["funkwhale"]["config"]["replace"] is True + + mocked_process.assert_called_once_with(upload_id=upload.pk) -def test_import_files_creates_a_batch_and_job(factories, mocker): - m = mocker.patch("funkwhale_api.music.tasks.import_job_run") - user = factories["users.User"](username="me") +def test_import_with_custom_reference(factories, mocker): + library = factories["music.Library"](actor__local=True) path = os.path.join(DATA_DIR, "dummy_file.ogg") - call_command("import_files", path, username="me", async=False, interactive=False) + mocked_process = mocker.patch("funkwhale_api.music.tasks.process_upload") + call_command( + "import_files", + str(library.uuid), + path, + reference="test", + replace=True, + interactive=False, + ) + upload = library.uploads.last() - batch = user.imports.latest("id") - assert batch.source == "shell" - assert batch.jobs.count() == 1 + assert upload.import_reference == "test" - job = batch.jobs.first() - - assert job.status == "pending" - with open(path, "rb") as f: - assert job.audio_file.read() == f.read() - - assert job.source == "file://" + path - m.assert_called_once_with(import_job_id=job.pk, use_acoustid=False) + mocked_process.assert_called_once_with(upload_id=upload.pk) def test_import_files_skip_if_path_already_imported(factories, mocker): - user = factories["users.User"](username="me") + library = factories["music.Library"](actor__local=True) path = os.path.join(DATA_DIR, "dummy_file.ogg") - factories["music.TrackFile"](source="file://{}".format(path)) - call_command("import_files", path, username="me", async=False, interactive=False) - assert user.imports.count() == 0 + # existing one with same source + factories["music.Upload"]( + library=library, import_status="finished", source="file://{}".format(path) + ) - -def test_import_files_works_with_utf8_file_name(factories, mocker): - m = mocker.patch("funkwhale_api.music.tasks.import_job_run") - user = factories["users.User"](username="me") - path = os.path.join(DATA_DIR, "utf8-éà◌.ogg") - call_command("import_files", path, username="me", async=False, interactive=False) - batch = user.imports.latest("id") - job = batch.jobs.first() - m.assert_called_once_with(import_job_id=job.pk, use_acoustid=False) + call_command( + "import_files", str(library.uuid), path, async=False, interactive=False + ) + assert library.uploads.count() == 1 def test_import_files_in_place(factories, mocker, settings): settings.MUSIC_DIRECTORY_PATH = DATA_DIR - m = mocker.patch("funkwhale_api.music.tasks.import_job_run") - user = factories["users.User"](username="me") + mocked_process = mocker.patch("funkwhale_api.music.tasks.process_upload") + library = factories["music.Library"](actor__local=True) path = os.path.join(DATA_DIR, "utf8-éà◌.ogg") call_command( "import_files", + str(library.uuid), path, - username="me", - async=False, + async_=False, in_place=True, interactive=False, ) - batch = user.imports.latest("id") - job = batch.jobs.first() - assert bool(job.audio_file) is False - m.assert_called_once_with(import_job_id=job.pk, use_acoustid=False) + upload = library.uploads.last() + assert bool(upload.audio_file) is False + mocked_process.assert_called_once_with(upload_id=upload.pk) def test_storage_rename_utf_8_files(factories): - tf = factories["music.TrackFile"](audio_file__filename="été.ogg") - assert tf.audio_file.name.endswith("ete.ogg") + upload = factories["music.Upload"](audio_file__filename="été.ogg") + assert upload.audio_file.name.endswith("ete.ogg") diff --git a/api/tests/test_youtube.py b/api/tests/test_youtube.py deleted file mode 100644 index cb5559ce1..000000000 --- a/api/tests/test_youtube.py +++ /dev/null @@ -1,96 +0,0 @@ -from collections import OrderedDict - -from django.urls import reverse - -from funkwhale_api.providers.youtube.client import client - -from .data import youtube as api_data - - -def test_can_get_search_results_from_youtube(mocker): - mocker.patch( - "funkwhale_api.providers.youtube.client._do_search", - return_value=api_data.search["8 bit adventure"], - ) - query = "8 bit adventure" - results = client.search(query) - assert results[0]["id"]["videoId"] == "0HxZn6CzOIo" - assert results[0]["snippet"]["title"] == "AdhesiveWombat - 8 Bit Adventure" - assert results[0]["full_url"] == "https://www.youtube.com/watch?v=0HxZn6CzOIo" - - -def test_can_get_search_results_from_funkwhale(preferences, mocker, api_client, db): - preferences["common__api_authentication_required"] = False - mocker.patch( - "funkwhale_api.providers.youtube.client._do_search", - return_value=api_data.search["8 bit adventure"], - ) - query = "8 bit adventure" - url = reverse("api:v1:providers:youtube:search") - response = api_client.get(url, {"query": query}) - # we should cast the youtube result to something more generic - expected = { - "id": "0HxZn6CzOIo", - "url": "https://www.youtube.com/watch?v=0HxZn6CzOIo", - "type": "youtube#video", - "description": "Description", - "channelId": "UCps63j3krzAG4OyXeEyuhFw", - "title": "AdhesiveWombat - 8 Bit Adventure", - "channelTitle": "AdhesiveWombat", - "publishedAt": "2012-08-22T18:41:03.000Z", - "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg", - } - - assert response.data[0] == expected - - -def test_can_send_multiple_queries_at_once(mocker): - mocker.patch( - "funkwhale_api.providers.youtube.client._do_search", - side_effect=[ - api_data.search["8 bit adventure"], - api_data.search["system of a down toxicity"], - ], - ) - - queries = OrderedDict() - queries["1"] = {"q": "8 bit adventure"} - queries["2"] = {"q": "system of a down toxicity"} - - results = client.search_multiple(queries) - - assert results["1"][0]["id"]["videoId"] == "0HxZn6CzOIo" - assert results["1"][0]["snippet"]["title"] == "AdhesiveWombat - 8 Bit Adventure" - assert results["1"][0]["full_url"] == "https://www.youtube.com/watch?v=0HxZn6CzOIo" - assert results["2"][0]["id"]["videoId"] == "BorYwGi2SJc" - assert results["2"][0]["snippet"]["title"] == "System of a Down: Toxicity" - assert results["2"][0]["full_url"] == "https://www.youtube.com/watch?v=BorYwGi2SJc" - - -def test_can_send_multiple_queries_at_once_from_funwkhale( - preferences, mocker, db, api_client -): - preferences["common__api_authentication_required"] = False - mocker.patch( - "funkwhale_api.providers.youtube.client._do_search", - return_value=api_data.search["8 bit adventure"], - ) - queries = OrderedDict() - queries["1"] = {"q": "8 bit adventure"} - - expected = { - "id": "0HxZn6CzOIo", - "url": "https://www.youtube.com/watch?v=0HxZn6CzOIo", - "type": "youtube#video", - "description": "Description", - "channelId": "UCps63j3krzAG4OyXeEyuhFw", - "title": "AdhesiveWombat - 8 Bit Adventure", - "channelTitle": "AdhesiveWombat", - "publishedAt": "2012-08-22T18:41:03.000Z", - "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg", - } - - url = reverse("api:v1:providers:youtube:searchs") - response = api_client.post(url, queries, format="json") - - assert expected == response.data["1"][0] diff --git a/api/tests/users/test_ldap.py b/api/tests/users/test_ldap.py new file mode 100644 index 000000000..1010d02c8 --- /dev/null +++ b/api/tests/users/test_ldap.py @@ -0,0 +1,22 @@ +from django.contrib.auth import get_backends + +from django_auth_ldap import backend + + +def test_ldap_user_creation_also_creates_actor(settings, factories, mocker): + actor = factories["federation.Actor"]() + mocker.patch("funkwhale_api.users.models.create_actor", return_value=actor) + mocker.patch( + "django_auth_ldap.backend.LDAPBackend.ldap_to_django_username", + return_value="hello", + ) + settings.AUTHENTICATION_BACKENDS += ("django_auth_ldap.backend.LDAPBackend",) + # django-auth-ldap offers a populate_user signal we can use + # to create our user actor if it does not exists + ldap_backend = get_backends()[-1] + ldap_user = backend._LDAPUser(ldap_backend, username="hello") + ldap_user._user_attrs = {"hello": "world"} + ldap_user._get_or_create_user() + ldap_user._user.refresh_from_db() + + assert ldap_user._user.actor == actor diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py index 39a5bd326..69d338828 100644 --- a/api/tests/users/test_models.py +++ b/api/tests/users/test_models.py @@ -133,23 +133,90 @@ def test_can_filter_closed_invitations(factories): def test_creating_actor_from_user(factories, settings): - user = factories["users.User"]() + user = factories["users.User"](username="Hello M. world") actor = models.create_actor(user) - assert actor.preferred_username == user.username + assert actor.preferred_username == "Hello_M_world" # slugified assert actor.domain == settings.FEDERATION_HOSTNAME assert actor.type == "Person" assert actor.name == user.username assert actor.manually_approves_followers is False - assert actor.url == federation_utils.full_url( - reverse("federation:actors-detail", kwargs={"user__username": user.username}) + assert actor.fid == federation_utils.full_url( + reverse( + "federation:actors-detail", + kwargs={"preferred_username": actor.preferred_username}, + ) ) assert actor.shared_inbox_url == federation_utils.full_url( - reverse("federation:actors-inbox", kwargs={"user__username": user.username}) + reverse("federation:shared-inbox") ) assert actor.inbox_url == federation_utils.full_url( - reverse("federation:actors-inbox", kwargs={"user__username": user.username}) + reverse( + "federation:actors-inbox", + kwargs={"preferred_username": actor.preferred_username}, + ) ) assert actor.outbox_url == federation_utils.full_url( - reverse("federation:actors-outbox", kwargs={"user__username": user.username}) + reverse( + "federation:actors-outbox", + kwargs={"preferred_username": actor.preferred_username}, + ) ) + assert actor.followers_url == federation_utils.full_url( + reverse( + "federation:actors-followers", + kwargs={"preferred_username": actor.preferred_username}, + ) + ) + assert actor.following_url == federation_utils.full_url( + reverse( + "federation:actors-following", + kwargs={"preferred_username": actor.preferred_username}, + ) + ) + + +def test_get_channels_groups(factories): + user = factories["users.User"]() + + assert user.get_channels_groups() == [ + "user.{}.imports".format(user.pk), + "user.{}.inbox".format(user.pk), + ] + + +def test_user_quota_default_to_preference(factories, preferences): + preferences["users__upload_quota"] = 42 + + user = factories["users.User"]() + assert user.get_upload_quota() == 42 + + +def test_user_quota_set_on_user(factories, preferences): + preferences["users__upload_quota"] = 42 + + user = factories["users.User"](upload_quota=66) + assert user.get_upload_quota() == 66 + + +def test_user_get_quota_status(factories, preferences, mocker): + user = factories["users.User"](upload_quota=66, with_actor=True) + mocker.patch( + "funkwhale_api.federation.models.Actor.get_current_usage", + return_value={ + "total": 10 * 1000 * 1000, + "pending": 1 * 1000 * 1000, + "skipped": 2 * 1000 * 1000, + "errored": 3 * 1000 * 1000, + "finished": 4 * 1000 * 1000, + }, + ) + assert user.get_quota_status() == { + "max": 66, + "remaining": 56, + "current": 10, + "pending": 1, + "skipped": 2, + "errored": 3, + "finished": 4, + } diff --git a/changes/template.rst b/changes/template.rst index 9ffcdc08e..c4a9b40e5 100644 --- a/changes/template.rst +++ b/changes/template.rst @@ -1,6 +1,6 @@ Upgrade instructions are available at -https://docs.funkwhale.audio/upgrading.html +https://docs.funkwhale.audio/index.html {% for section, _ in sections.items() %} {% if sections[section] %} diff --git a/demo/setup.sh b/demo/setup.sh index 5edbd692c..a19f6a7d4 100755 --- a/demo/setup.sh +++ b/demo/setup.sh @@ -12,6 +12,9 @@ mkdir -p $demo_path echo 'Downloading demo files...' curl -L -o docker-compose.yml "https://code.eliotberriot.com/funkwhale/funkwhale/raw/$version/deploy/docker-compose.yml" curl -L -o .env "https://code.eliotberriot.com/funkwhale/funkwhale/raw/$version/deploy/env.prod.sample" +mkdir nginx +curl -L -o nginx/funkwhale.template "https://code.eliotberriot.com/funkwhale/funkwhale/raw/$version/deploy/docker.nginx.template" +curl -L -o nginx/funkwhale_proxy.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/$version/deploy/funkwhale_proxy.conf" mkdir data/ curl -L -o front.zip "https://code.eliotberriot.com/funkwhale/funkwhale/-/jobs/artifacts/$version/download?job=build_front" @@ -23,6 +26,7 @@ echo "MUSIC_DIRECTORY_SERVE_PATH=$music_path" >> .env echo "MUSIC_DIRECTORY_PATH=$music_path" >> .env echo "MEDIA_ROOT=$demo_path/data/media/" >> .env echo "STATIC_ROOT=$demo_path/data/static/" >> .env +echo "FUNKWHALE_FRONTEND_PATH=$demo_path/front/dist/" >> .env # /usr/local/bin/docker-compose pull /usr/local/bin/docker-compose up -d postgres redis @@ -43,6 +47,8 @@ u = User.objects.create(email="demo@demo.com", username="demo", is_staff=True, i u.set_password("demo") u.subsonic_api_token = "demo" u.save() +actor = u.create_actor() +library = actor.libraries.create(name='Demo library', privacy_level='everyone') from funkwhale_api.common import preferences @@ -57,7 +63,7 @@ paths = [ "$music_path/**/*.flac", ] print(paths) -call_command("import_files", *paths, username="demo", recursive=True, interactive=False) +call_command("import_files", str(library.uuid), *paths, username="demo", recursive=True, interactive=False) print('Creating some dummy data...') @@ -69,7 +75,7 @@ from funkwhale_api.favorites.factories import TrackFavorite as TrackFavoriteFact from funkwhale_api.users.factories import UserFactory from funkwhale_api.playlists.factories import PlaylistFactory -users = UserFactory.create_batch(size=15, privacy_level="everyone") +users = UserFactory.create_batch(size=15, privacy_level="everyone", with_actor=True) available_tracks = list(Track.objects.all()) available_albums = list(Album.objects.all()) diff --git a/deploy/FreeBSD/README.md b/deploy/FreeBSD/README.md new file mode 100644 index 000000000..5880f2b39 --- /dev/null +++ b/deploy/FreeBSD/README.md @@ -0,0 +1,21 @@ +### System V rc script for FreeBSD + +Copy the file in `/usr/local/etc/rc.d` + +``` +# cp /path/to/funkwhale/deploy/FreeBSD/funkwhale_* /usr/local/etc/rc.d +``` + +If not add executable bit to the files. + +``` +# chmod +x /usr/local/etc/rc.d/funkwhale_* +``` + +Enable services in rc.conf + +``` +# sysrc funkwhale_server=YES +# sysrc funkwhale_worker=YES +# sysrc funkwhale_beat=YES +``` diff --git a/deploy/FreeBSD/funkwhale_beat b/deploy/FreeBSD/funkwhale_beat new file mode 100755 index 000000000..6a2b37b75 --- /dev/null +++ b/deploy/FreeBSD/funkwhale_beat @@ -0,0 +1,35 @@ +#!/bin/sh + +# PROVIDE: funkwhale_beat +# REQUIRE: LOGIN postgresql nginx redis +# KEYWORD: shutdown +# +# Add the following lines to /etc/rc.conf to enable funkwhale_beat: +# funkwhale_beat (bool): Set it to "YES" to enable Funkwhale task beat. +# Default is "NO". + + +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin + +. /etc/rc.subr + +desc="Funkwhale beat" +name=funkwhale_beat +rcvar=funkwhale_beat_enable + +load_rc_config $name + +: ${funkwhale_beat_enable:=NO} + +funkwhale_beat_chdir="/usr/local/www/funkwhale/api" +funkwhale_beat_user=funkwhale +funkwhale_beat_env=$(cat /usr/local/www/funkwhale/config/.env | grep -v ^# | xargs) +pidfile="/var/run/funkwhale/${name##funkwhale_}.pid" +command_interpreter="/usr/local/www/funkwhale/virtualenv/bin/python3" + +command="/usr/local/www/funkwhale/virtualenv/bin/celery" +command_args="-A funkwhale_api.taskapp beat -l INFO \ +--pidfile=${pidfile} \ +>> /var/log/funkwhale/${name##funkwhale_}.log 2>&1 &" + +run_rc_command "$1" diff --git a/deploy/FreeBSD/funkwhale_server b/deploy/FreeBSD/funkwhale_server new file mode 100755 index 000000000..1d7f29ce5 --- /dev/null +++ b/deploy/FreeBSD/funkwhale_server @@ -0,0 +1,32 @@ +#!/bin/sh + +# PROVIDE: funkwhale_server +# REQUIRE: LOGIN postgresql nginx redis +# KEYWORD: shutdown +# +# Add the following lines to /etc/rc.conf to enable funkwhale_server: +# funkwhale_server (bool): Set it to "YES" to enable Funkwhale web server. +# Default is "NO". + +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin + +. /etc/rc.subr + +desc="Funkwhale server" +name=funkwhale_server +rcvar=funkwhale_server_enable + +load_rc_config $name + +: ${funkwhale_server_enable:=NO} + +funkwhale_server_chdir="/usr/local/www/funkwhale/api" +funkwhale_server_user=funkwhale +funkwhale_server_env=$(cat /usr/local/www/funkwhale/config/.env | grep -v ^# | xargs) +command_interpreter="/usr/local/www/funkwhale/virtualenv/bin/python3" + +command="/usr/local/www/funkwhale/virtualenv/bin/daphne" +command_args="-b 127.0.0.1 -p 5000 config.asgi:application --proxy-headers \ +>> /var/log/funkwhale/${name##funkwhale_}.log 2>&1 &" + +run_rc_command "$1" diff --git a/deploy/FreeBSD/funkwhale_worker b/deploy/FreeBSD/funkwhale_worker new file mode 100755 index 000000000..e9aab90fd --- /dev/null +++ b/deploy/FreeBSD/funkwhale_worker @@ -0,0 +1,34 @@ +#!/bin/sh + +# PROVIDE: funkwhale_worker +# REQUIRE: LOGIN postgresql nginx redis +# KEYWORD: shutdown +# +# Add the following lines to /etc/rc.conf to enable funkwhale_worker: +# funkwhale_worker (bool): Set it to "YES" to enable Funkwhale task worker. +# Default is "NO". + +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin + +. /etc/rc.subr + +desc="Funkwhale worker" +name=funkwhale_worker +rcvar=funkwhale_worker_enable + +load_rc_config $name + +: ${funkwhale_worker_enable:=NO} + +funkwhale_worker_chdir="/usr/local/www/funkwhale/api" +funkwhale_worker_user=funkwhale +funkwhale_worker_env=$(cat /usr/local/www/funkwhale/config/.env | grep -v ^# | xargs) +pidfile="/var/run/funkwhale/${name##funkwhale_}.pid" +command_interpreter="/usr/local/www/funkwhale/virtualenv/bin/python3" + +command="/usr/local/www/funkwhale/virtualenv/bin/celery" +command_args="-A funkwhale_api.taskapp worker -l INFO \ +--pidfile=${pidfile} \ +>> /var/log/funkwhale/${name##funkwhale_}.log 2>&1 &" + +run_rc_command "$1" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 302e6da21..815c4e5bb 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,7 +1,6 @@ -version: '3' +version: "3" services: - postgres: restart: unless-stopped env_file: .env @@ -55,25 +54,35 @@ services: - "${MUSIC_DIRECTORY_SERVE_PATH-/srv/funkwhale/data/music}:${MUSIC_DIRECTORY_PATH-/music}:ro" - "${MEDIA_ROOT}:${MEDIA_ROOT}" - "${STATIC_ROOT}:${STATIC_ROOT}" - - ./front/dist:/frontend + - "${FUNKWHALE_FRONTEND_PATH}:/frontend" ports: - - "${FUNKWHALE_API_IP:-127.0.0.1}:${FUNKWHALE_API_PORT:-5000}:5000" + - "5000" links: - postgres - redis - # If you want to have the nginx proxy managed by docker for some reason - # (i.e. if you use apache as a proxy on your host), - # you can uncomment the following lines. - # nginx: - # image: nginx - # links: - # - api - # volumes: - # - ./nginx.conf:/etc/nginx/conf.d/funkwhale.conf:ro - # - ./funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro - # - ./data/media:/srv/funkwhale/data/media:ro - # - ./front/dist:/srv/funkwhale/front/dist:ro - # - ./data/static:/srv/funkwhale/data/static/:ro - # ports: - # - "127.0.0.1:5001:80" + nginx: + image: nginx + env_file: + - .env + environment: + # Override those variables in your .env file if needed + - "NGINX_MAX_BODY_SIZE=${NGINX_MAX_BODY_SIZE-30M}" + volumes: + - "./nginx/funkwhale.template:/etc/nginx/conf.d/funkwhale.template:ro" + - "./nginx/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro" + - "${MUSIC_DIRECTORY_SERVE_PATH-/srv/funkwhale/data/music}:${MUSIC_DIRECTORY_SERVE_PATH-/srv/funkwhale/data/music}:ro" + - "${MEDIA_ROOT}:${MEDIA_ROOT}:ro" + - "${STATIC_ROOT}:${STATIC_ROOT}:ro" + - "${FUNKWHALE_FRONTEND_PATH}:/frontend:ro" + ports: + # override those variables in your .env file if needed + - "${FUNKWHALE_API_IP}:${FUNKWHALE_API_PORT}:80" + command: > + sh -c "envsubst \"`env | awk -F = '{printf \" $$%s\", $$1}'`\" + < /etc/nginx/conf.d/funkwhale.template + > /etc/nginx/conf.d/default.conf + && cat /etc/nginx/conf.d/default.conf + && nginx -g 'daemon off;'" + links: + - api diff --git a/deploy/docker.funkwhale_proxy.conf b/deploy/docker.funkwhale_proxy.conf new file mode 100644 index 000000000..08a080d58 --- /dev/null +++ b/deploy/docker.funkwhale_proxy.conf @@ -0,0 +1,20 @@ +# use this one if you put the nginx container behind another proxy +# you will have to set some headers on this proxy as well to ensure +# everything works correctly, you can use the ones from the funkwhale_proxy.conf file +# at https://code.eliotberriot.com/funkwhale/funkwhale/blob/develop/deploy/funkwhale_proxy.conf +# your proxy will also need to support websockets + +real_ip_header X-Forwarded-For; +set_real_ip_from 172.17.0.0/16; + +proxy_set_header Host $http_x_forwarded_host; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; +proxy_set_header X-Forwarded-Host $http_x_forwarded_host; +proxy_set_header X-Forwarded-Port $http_x_forwarded_port; +proxy_redirect off; + +# websocket support +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection $connection_upgrade; diff --git a/deploy/docker.nginx.template b/deploy/docker.nginx.template new file mode 100644 index 000000000..d73a1c4b6 --- /dev/null +++ b/deploy/docker.nginx.template @@ -0,0 +1,81 @@ +upstream funkwhale-api { + # depending on your setup, you may want to update this + server api:5000; +} + + +# required for websocket support +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + server_name ${FUNKWHALE_HOSTNAME}; + + # TLS + # Feel free to use your own configuration for SSL here or simply remove the + # lines and move the configuration to the previous server block if you + # don't want to run funkwhale behind https (this is not recommended) + # have a look here for let's encrypt configuration: + # https://certbot.eff.org/all-instructions/#debian-9-stretch-nginx + + root /frontend; + + location / { + try_files $uri $uri/ @rewrites; + } + + location @rewrites { + rewrite ^(.+)$ /index.html last; + } + location /api/ { + include /etc/nginx/funkwhale_proxy.conf; + # this is needed if you have file import via upload enabled + client_max_body_size ${NGINX_MAX_BODY_SIZE}; + proxy_pass http://funkwhale-api/api/; + } + + location /federation/ { + include /etc/nginx/funkwhale_proxy.conf; + proxy_pass http://funkwhale-api/federation/; + } + + # You can comment this if you do not plan to use the Subsonic API + location /rest/ { + include /etc/nginx/funkwhale_proxy.conf; + proxy_pass http://funkwhale-api/api/subsonic/rest/; + } + + location /.well-known/ { + include /etc/nginx/funkwhale_proxy.conf; + proxy_pass http://funkwhale-api/.well-known/; + } + + location /media/ { + alias ${MEDIA_ROOT}/; + } + + location /_protected/media { + # this is an internal location that is used to serve + # audio files once correct permission / authentication + # has been checked on API side + internal; + alias ${MEDIA_ROOT}; + } + + location /_protected/music { + # this is an internal location that is used to serve + # audio files once correct permission / authentication + # has been checked on API side + # Set this to the same value as your MUSIC_DIRECTORY_PATH setting + internal; + alias ${MUSIC_DIRECTORY_PATH}; + } + + location /staticfiles/ { + # django static files + alias ${STATIC_ROOT}/; + } +} diff --git a/deploy/docker.proxy.template b/deploy/docker.proxy.template new file mode 100644 index 000000000..319f287e7 --- /dev/null +++ b/deploy/docker.proxy.template @@ -0,0 +1,36 @@ +upstream fw { + # depending on your setup, you may want to update this + server ${FUNKWHALE_API_IP}:${FUNKWHALE_API_PORT}; +} +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + listen [::]:80; + server_name ${FUNKWHALE_HOSTNAME}; + location / { return 301 https://$host$request_uri; } +} +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name ${FUNKWHALE_HOSTNAME}; + + # TLS + ssl_protocols TLSv1.2; + ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_certificate /etc/letsencrypt/live/${FUNKWHALE_HOSTNAME}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${FUNKWHALE_HOSTNAME}/privkey.pem; + + # HSTS + add_header Strict-Transport-Security "max-age=31536000"; + + location / { + include /etc/nginx/funkwhale_proxy.conf; + proxy_pass http://fw/; + } +} diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index 838e8fef4..3918f95e3 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -5,7 +5,7 @@ # following variables: # - DJANGO_SECRET_KEY # - DJANGO_ALLOWED_HOSTS -# - FUNKWHALE_URL +# - FUNKWHALE_HOSTNAME # - EMAIL_CONFIG and DEFAULT_FROM_EMAIL if you plan to send emails) # On non-docker setup **only**, you'll also have to tweak/uncomment those variables: # - DATABASE_URL @@ -38,7 +38,8 @@ FUNKWHALE_API_PORT=5000 # Replace this by the definitive, public domain you will use for # your instance -FUNKWHALE_URL=https://yourdomain.funwhale +FUNKWHALE_HOSTNAME=yourdomain.funkwhale +FUNKWHALE_PROTOCOL=https # Configure email sending using this variale # By default, funkwhale will output emails sent to stdout @@ -50,7 +51,7 @@ FUNKWHALE_URL=https://yourdomain.funwhale # EMAIL_CONFIG=smtp+ssl://user@:password@youremail.host:465 # EMAIL_CONFIG=smtp+tls://user@:password@youremail.host:587 -# The email address to use to send systme emails. By default, we will +# The email address to use to send system emails. # DEFAULT_FROM_EMAIL=noreply@yourdomain # Depending on the reverse proxy used in front of your funkwhale instance, @@ -116,3 +117,25 @@ RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f # Typical non-docker setup: # MUSIC_DIRECTORY_PATH=/srv/funkwhale/data/music # # MUSIC_DIRECTORY_SERVE_PATH= # stays commented, not needed + +MUSIC_DIRECTORY_PATH=/srv/funkwhale/data/music +MUSIC_DIRECTORY_SERVE_PATH=/srv/funkwhale/data/music + +# LDAP settings +# Use the following options to allow authentication on your Funkwhale instance +# using a LDAP directory. +# Have a look at https://docs.funkwhale.audio/installation/ldap.html for +# detailed instructions. + +# LDAP_ENABLED=False +# LDAP_SERVER_URI=ldap://your.server:389 +# LDAP_BIND_DN=cn=admin,dc=domain,dc=com +# LDAP_BIND_PASSWORD=bindpassword +# LDAP_SEARCH_FILTER=(|(cn={0})(mail={0})) +# LDAP_START_TLS=False +# LDAP_ROOT_DN=dc=domain,dc=com + +FUNKWHALE_FRONTEND_PATH=/srv/funkwhale/front/dist + +# Nginx related configuration +NGINX_MAX_BODY_SIZE=30M diff --git a/deploy/nginx.conf b/deploy/nginx.template similarity index 78% rename from deploy/nginx.conf rename to deploy/nginx.template index 2d8412edb..1eb011d4e 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.template @@ -1,16 +1,15 @@ -# Ensure you update at least the server_name variables to match your own +# This file was generated from funkwhale.template -# domain upstream funkwhale-api { - # depending on your setup, you may want to udpate this - server localhost:5000; + # depending on your setup, you may want to update this + server ${FUNKWHALE_API_IP}:${FUNKWHALE_API_PORT}; } server { listen 80; listen [::]:80; # update this to match your instance name - server_name demo.funkwhale.audio; + server_name ${FUNKWHALE_HOSTNAME}; # useful for Let's Encrypt location /.well-known/acme-challenge/ { allow all; } location / { return 301 https://$host$request_uri; } @@ -25,8 +24,7 @@ map $http_upgrade $connection_upgrade { server { listen 443 ssl http2; listen [::]:443 ssl http2; - # update this to match your instance name - server_name demo.funkwhale.audio; + server_name ${FUNKWHALE_HOSTNAME}; # TLS # Feel free to use your own configuration for SSL here or simply remove the @@ -38,12 +36,12 @@ server { ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; - ssl_certificate /etc/letsencrypt/live/demo.funkwhale.audio/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/demo.funkwhale.audio/privkey.pem; + ssl_certificate /etc/letsencrypt/live/${FUNKWHALE_HOSTNAME}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${FUNKWHALE_HOSTNAME}/privkey.pem; # HSTS add_header Strict-Transport-Security "max-age=31536000"; - root /srv/funkwhale/front/dist; + root ${FUNKWHALE_FRONTEND_PATH}; location / { try_files $uri $uri/ @rewrites; @@ -55,7 +53,7 @@ server { location /api/ { include /etc/nginx/funkwhale_proxy.conf; # this is needed if you have file import via upload enabled - client_max_body_size 30M; + client_max_body_size ${NGINX_MAX_BODY_SIZE}; proxy_pass http://funkwhale-api/api/; } @@ -76,7 +74,7 @@ server { } location /media/ { - alias /srv/funkwhale/data/media/; + alias ${MEDIA_ROOT}/; } location /_protected/media { @@ -84,7 +82,7 @@ server { # audio files once correct permission / authentication # has been checked on API side internal; - alias /srv/funkwhale/data/media; + alias ${MEDIA_ROOT}; } location /_protected/music { @@ -93,11 +91,11 @@ server { # has been checked on API side # Set this to the same value as your MUSIC_DIRECTORY_PATH setting internal; - alias /srv/funkwhale/data/music; + alias ${MUSIC_DIRECTORY_SERVE_PATH}; } location /staticfiles/ { # django static files - alias /srv/funkwhale/data/static/; + alias ${STATIC_ROOT}/; } } diff --git a/dev.yml b/dev.yml index 1d9cbba20..5ac74424c 100644 --- a/dev.yml +++ b/dev.yml @@ -10,7 +10,7 @@ services: - "HOST=0.0.0.0" - "VUE_PORT=${VUE_PORT-8080}" ports: - - "${VUE_PORT_BINDING-8080:}${VUE_PORT-8080}" + - "${VUE_PORT-8080}:${VUE_PORT-8080}" volumes: - "./front:/app" - "/app/node_modules" @@ -30,6 +30,7 @@ services: - .env.dev - .env image: postgres + command: postgres -c log_min_duration_statement=0 volumes: - "./data/${COMPOSE_PROJECT_NAME-node1}/postgres:/var/lib/postgresql/data" networks: diff --git a/docker/nginx/entrypoint.sh b/docker/nginx/entrypoint.sh index 9914a7aae..f359f4da9 100755 --- a/docker/nginx/entrypoint.sh +++ b/docker/nginx/entrypoint.sh @@ -9,7 +9,7 @@ if [ -n "$COMPOSE_PROJECT_NAME" ]; then fi echo "Copying template file..." cp /etc/nginx/funkwhale_proxy.conf{.template,} -sed -i "s/X-Forwarded-Host \$host:\$server_port/X-Forwarded-Host ${FUNKWHALE_HOSTNAME}:${FORWARDED_PORT}/" /etc/nginx/funkwhale_proxy.conf +sed -i "s/X-Forwarded-Host \$host:\$server_port/X-Forwarded-Host ${FUNKWHALE_HOSTNAME}/" /etc/nginx/funkwhale_proxy.conf sed -i "s/proxy_set_header Host \$host/proxy_set_header Host ${FUNKWHALE_HOSTNAME}/" /etc/nginx/funkwhale_proxy.conf sed -i "s/proxy_set_header X-Forwarded-Port \$server_port/proxy_set_header X-Forwarded-Port ${FORWARDED_PORT}/" /etc/nginx/funkwhale_proxy.conf sed -i "s/proxy_set_header X-Forwarded-Proto \$scheme/proxy_set_header X-Forwarded-Proto ${FORWARDED_PROTO}/" /etc/nginx/funkwhale_proxy.conf diff --git a/docs/features.rst b/docs/features.rst index c57e229fa..7fefa70b8 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -4,7 +4,7 @@ Features Scope ------ -Funkwhale is a web based music server. It is similar in term of goals and feature set to various existing projects, such as `Sonerezh `_ or `Libresonic `_. +Funkwhale is a web based music server. It is similar in term of goals and feature set to various existing projects, such as `Sonerezh `_ or `Airsonic `_. A social platform ------------------ diff --git a/docs/federation.rst b/docs/federation.rst deleted file mode 100644 index 08658b12e..000000000 --- a/docs/federation.rst +++ /dev/null @@ -1,56 +0,0 @@ -Federation -========== - -Each Funkwale instance can federates its music library with other instances -of the network. This means that an instance A can acquire music from instance B -and share its own library with an instance C. - -We support various levels of controls for federation-related features. - -Managing federation -------------------- - -Federation management is only available to instance admins and users -who have the proper permissions. You can disable federation completely -at the instance level by editing the ``federation__enabled`` :ref:`setting `. - -On the front end, assuming you have the proper permission, you will see -a "Federation" link in the sidebar. - - -Acquire music via federation ----------------------------- - -Instance libraries are protected by default. To access another instance -library, you have to follow it. Each Funkwhale instance gets a dedicated -ActivityPub Actor you can follow via the username "library@yourinstance.domain". - -When submitted, a follow request will be sent to -the other instance which can accept or deny it. Once your follow request -is accepted, you can start browsing the other instance library -and import music from it. - -By default, we do not duplicate audio files from federated tracks, to reduce -disk usage on your instance. When someone listens to a federated track, -the audio file is requested on the fly from the remote instance, and -store in a local cache. It is automatically deleted after a configurable -amount of time if it was not listened again in the meantime. - -If you want to mirror a remote instance collection, including its audio files, -we offer an option for that. - -We also support an "autoimport" mode for each remote library. When enabled, -any new track published in the remote library will be directly imported -in your instance. - -Share music via federation --------------------------- - -Federation is enabled by default, but requires manually approving -each other instance asking for access to library. This is by design, -to ensure your library is not shared publicly without your consent. - -However, if you're confident about federating publicly without manual approval, -you can set the ``federation__music_needs_approval`` :ref:`setting ` to false. -Follow requests will be accepted automatically and followers -given access to your library without manual intervention. diff --git a/docs/federation/index.rst b/docs/federation/index.rst new file mode 100644 index 000000000..cbc8cd155 --- /dev/null +++ b/docs/federation/index.rst @@ -0,0 +1,615 @@ +Funkwhale Federation +==================== + +This documentation section is more technical, and targets people who want +to understand how Funkwhale's federation works. + + +Technologies and standards +-------------------------- + +Funkwhale's federation is built on top of the following technologies: + +- `ActivityPub`_ as the high-level federation protocol +- `HTTP Signatures`_ as the primary mean to authenticate messages +- `Webfinger`_ to easily retrive resources using human-friendly names +- `ActivityStreams`_ and `ActivityStreams vocabulary`_ as the mean to structure messages + +Support for the following is planned but not implemented-yet: + +- `JSON-LD signatures`_ as an alternate mean to authentify messages + +.. _ActivityPub: https://www.w3.org/TR/activitypub/ +.. _HTTP Signatures: https://tools.ietf.org/id/draft-cavage-http-signatures-01.html +.. _Webfinger: https://tools.ietf.org/html/rfc7033 +.. _JSON-LD signatures: https://w3c-dvcg.github.io/ld-signatures/ +.. _ActivityStreams: https://www.w3.org/TR/activitystreams-core/ +.. _ActivityStreams vocabulary: https://www.w3.org/TR/activitystreams-vocabulary/ + +Philosophy +---------- + +Our goal is to stick to the specifications as much as possible, to ensure +compatibility with existing applications such as Mastodon, Peertube, Plume, Pleroma or PixelFed. + +However, this is not always possible for all our use cases. The ActivityPub and ActivityStreams specifications +are really high-level and do not always fit our use cases. For such cases, we will +use an ad-hoc solution, and document it here. + +There are plenty of projects built using ActivityPub, and our goal is not to support all +the existing activities. Instead, we want to support activities and objects that make sense +for Funkwhale use cases, such as follows or likes. + +If you think this document is not accurate or find evidence that Funkwhale is not +behaving according to the behaviour documented here, please file a bug on our +issue tracker, as we consider this a bug. + +Internal logic +-------------- + +This section is relevant if you're interested in how we handle things internally +in our application code. + +Database schema +^^^^^^^^^^^^^^^ + +As much as possible, we try to map our internal model and database schema to +ActivityPub entities, as this makes things easier to deal with. + +We store received activities payload directly in the database before we attempt to process +or deliver them. Storing the activities unlock some interesting use cases, such as +debugging federation issues, replaying deliveries, or reprocess historical +activities that were not supported before. + +Each local user is bound to an ``Actor``. Remote and local actors share the same +database table and all federated entities (such as uploads) are linked to an ``Actor`` +and not to a user. This means that, internally, in general, there is no distinction between +local and remote users. + +Links: + +- `Federation models `_ + + +Activity creation and delivery +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When a local actor is making an action that should trigger an ``Activity``, which roughly is equivalent +to posting an activity to an outbox, we create an object, with the proper payload and store it in our +``Activity`` table. We then trigger two kind of deliveries: + +1. A delivery to local recipients: for each local recipient, we create an ``InboxItem``, linked to the activity. A local + actor's feed is then made of all the available inbox items, which can also have a read/unread + status +2. A delivery to remote recipients: we collect all inboxes and shared inbox urls from remote recipients, + and create a ``Delivery`` object in our database, linked to the initial activity and the inbox or shared inbox url. + This ``Delivery`` object is then used by our worker to post the activity content to the url. + +Receiving an activity from a remote actor in a local inbox is basically the same, but we skip 2#. + +Funkwhale does not support all activities, and we have a basic routing logic to handle +specific activities, and discard unsupported ones. Unsupported activities are still +received and stored though. + +If a delivered activity match one of our routes, a dedicated handler is called, +which can trigger additionnal logic. For instance, if we receive a :ref:`activity-create` activity +for an :ref:`object-audio` object, our handler will persist the proper data in our local ``Upload`` +table, retrieve the audio cover, etc. + +Links: + +- `Routing logic for activities `_ +- `Delivery logic for activities `_ + + +Supported activities +-------------------- + +.. _activity-follow: + +Follow +^^^^^^ + +Supported on +************ + +- :ref:`object-Library` objects + +Example of library follow +************************* + +.. code-block:: json + + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "type": "Follow", + "id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded", + "actor": "https://music.rocks/federation/actors/Alice", + "to": ["https://awesome.music/federation/actors/Bob"], + "object": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6" + } + +In this example, Alice is following the :ref:`object-library` described in ``object``, which is +owned by Bob. + +Internal logic +************** + +When a follow is received on a :ref:`object-Library`, Funkwhale will behave differently +depending on the visibility of the library: + +- Automatic accept, when the library is public: a notification is sent to the library owner, and an :ref:`activity-accept` is sent automatically to the follow actor. +- Manuel accept, in all other cases: a notification is sent to the library owner. After manual approval from the owner, an :ref:`activity-accept` is sent to the follow actor. + +Funkwhale uses library follows status to grant access to the follow actor. If a library +is not public and an actor does not have an approved follow, library content must be +inaccessible to the actor. + +Checks +****** + +Before handling the activity, Funkwhale will ensure the library's owner is +the activity recipient. + +.. _activity-accept: + +Accept +^^^^^^ + +Supported on +************ + +- :ref:`activity-follow` objects + +Example +******* + +.. code-block:: json + + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "type": "Accept", + "id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded/accept", + "to": ["https://music.rocks/federation/actors/Alice"], + "actor": "https://awesome.music/federation/actors/Bob", + "object": { + "id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded", + "type": "Follow", + "actor": "https://music.rocks/federation/actors/Alice", + "object": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6", + }, + } + +In this example, Bob accepts Alice's follow. + +Internal logic +************** + +When an :ref:`activity-accept` is received with a :ref:`activity-follow` object, the corresponding follow +is marked as accepted in the database. + +For library follows, this means that the actor will receive future +activities occuring within this library, such as :ref:`activity-create` :ref:`object-audio`, +:ref:`activity-delete` :ref:`object-audio` or :ref:`activity-delete` :ref:`object-library` + +The follow actor will also be able to browse the library pages and download the library's +audio files. Have a look at :ref:`library-access` for more details. + + +Checks +****** + +Before handling the activity, Funkwhale will ensure the accept comes from +the library's owner. + +.. _activity-undo: + +Undo +^^^^ + +Supported on +************ + +- :ref:`activity-follow` objects + +Example +******* + +.. code-block:: json + + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "type": "Undo", + "id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded/accept", + "to": ["https://awesome.music/federation/actors/Bob"], + "actor": "https://music.rocks/federation/actors/Alice", + "object": { + "id": "https://music.rocks/federation/actors/Alice#follows/99fc40d7-9bc8-4c4a-add1-f637339e1ded", + "type": "Follow", + "actor": "https://music.rocks/federation/actors/Alice", + "object": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6", + }, + } + +In this example, Alice is notifying Bob she's undoing her follow. + +Internal logic +************** + +When an undo is received, the corresponding follow is deleted from the database. + +Checks +****** + +Before handling the activity, Funkwhale will ensure the undo actor is the +follow actor. + +.. _activity-create: + +Create +^^^^^^ + +Supported on +************ + +- :ref:`object-audio` objects + +Example +******* + +.. code-block:: json + + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "to": [ + "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6/followers" + ], + "type": "Create", + "actor": "https://awesome.music/federation/actors/Bob", + "object": {} + } + +.. note:: + + Refer to :ref:`object-audio` to see the structure of the ``object`` attribute. + +Internal logic +************** + +When a :ref:`activity-create` is received with an :ref:`object-audio` object, Funkwhale will persist +a local upload and bind it to the proper library and track. If no local track +match the audio metadata, a track is created using the ``metadata`` attribute +from the :ref:`object-audio` object. + +Checks +****** + +Before handling the activity, Funkwhale will ensure the activity actor and +the audio library's actor are the same. + +If no local actor follows the audio's library, the activity will be discarded. + +.. _activity-delete: + +Delete +^^^^^^ + +Supported on +************ + +- :ref:`object-audio` objects +- :ref:`object-Library` objects + +Example (on :ref:`object-Library`) +************************ + +.. code-block:: json + + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "type": "Delete", + "to": [ + "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6/followers" + ], + "actor": "https://awesome.music/federation/actors/Bob", + "object": { + "type": "Library", + "id": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6" + } + } + +Example (on :ref:`object-audio`) +********************** + +.. code-block:: json + + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "type": "Delete", + "to": [ + "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6/followers" + ], + "actor": "https://awesome.music/federation/actors/Bob", + "object": { + "type": "Audio", + "id": [ + "https://awesome.music/federation/music/uploads/19420073-3572-48a9-8c6c-b385ee1b7905", + "https://awesome.music/federation/music/uploads/11d99680-23c6-4f72-997a-073b980ab204", + "https://awesome.music/federation/music/uploads/1efadc1c-a704-4b8a-a71a-b288b1d1f423" + ] + } + } + +In this example, Bob notifies the followers of their library that 3 objects were deleted. + +.. note:: + + For performance reason, when deleting :ref:`object-audio` objects, Funkwhale support + either a list of ids or a single id. + +Internal logic +************** + +When a :ref:`activity-delete` is received, the corresponding objects are deleted immediatly +from the database. + +Checks +****** + +Before handling deletion, Funkwhale ensure the actor initiating the activity +is the owner of the deleted :ref:`object-audio` or :ref:`object-Library`. + +Supported objects +----------------- + +.. _object-artist: + +Artist +^^^^^^ + +.. note:: + + This object is not standard. + +Example +******* + +.. code-block:: json + + { + "type": "Artist", + "id": "https://awesome.music/federation/music/artists/73c32807-a199-4682-8068-e967f734a320", + "name": "Metallica", + "published": "2018-04-08T12:19:05.920415+00:00", + "musicbrainzId": "65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab" + } + +Structure +********* + +- **id** (required): a uri identifying the artist over federation +- **name** (required): a name for the artist +- **published** (required): the publication date of the artist (on the federation) +- **musicbrainzId** (optional): the musicbrainz artist id + +.. _object-album: + +Album +^^^^^ + +.. note:: + + This object is not standard. + +Example +******* + +.. code-block:: json + + { + "type": "Album", + "id": "https://awesome.music/federation/music/albums/69d488b5-fdf6-4803-b47c-9bb7098ea57e", + "name": "Ride the Lightning", + "released": "1984-01-01", + "published": "2018-10-02T19:49:17.412546+00:00", + "musicbrainzId": "589ff96d-0be8-3f82-bdd2-299592e51b40", + "cover": { + "href": "https://awesome.music/media/albums/covers/2018/10/02/b69d398b5-fdf6-4803-b47c-9bb7098ea57e.jpg", + "type": "Link", + "mediaType": "image/jpeg" + }, + "artists": [ + {} + ] + } + +Structure +********* + +- **id** (required): a uri identifying the album over federation +- **name** (required): the title of the album +- **artists** (required): a list of :ref:`object-artist` objects involved in the album +- **published** (required): the publication date of the entity (on the federation) +- **released** (optional): the release date of the album +- **musicbrainzId** (optional): the musicbrainz release id +- **cover** (optional): a `Link` object representing the album cover + +.. _object-track: + +Track +^^^^^ + +.. note:: + + This object is not standard. + +Example +******* + +.. code-block:: json + + { + "type": "Track", + "id": "https://awesome.music/federation/music/tracks/82ece296-6397-4e26-be90-bac5f9990240", + "name": "For Whom the Bell Tolls", + "position": 3, + "published": "2018-10-02T19:49:35.822537+00:00", + "musicbrainzId": "771ab043-8821-44f9-b8e0-2733c3126c6d", + "artists": [ + {} + ], + "album": {} + } + +Structure +********* + +- **id** (required): a uri identifying the track over federation +- **name** (required): the title of the track +- **position** (required): the position of the :ref:`object-track` in the album +- **published** (required): the publication date of the entity (on the federation) +- **musicbrainzId** (optional): the musicbrainz recording id +- **album** (required): the :ref:`object-album` that contains the track +- **artists** (required): a list of :ref:`object-artist` objects involved in the track (they can differ fro mthe album artists) + +.. _object-library: + +Library +^^^^^^^ + +.. note:: + + This object is not standard but inherits its behaviour and properties from + Actor and Collection. + + +Example +******* + +.. code-block:: json + + { + "type": "Library", + "id": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6", + "actor": "https://awesome.music/federation/actors/MyNameIsTroll", + "name": "My awesome library", + "followers": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6/followers", + "summary": "This library is for restricted use only", + "totalItems": 4234, + "first": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6?page=1", + "last": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6?page=56", + } + + +Structure +********* + +- **id** (required): a uri identifying the library over federation +- **actor** (required): the id of the actor managing the library +- **name** (required): the name of the library +- **followers** (required): the id of the library's followers collection +- **totalItems** (required): the number of audio objects available in the library +- **first** (required): the URL of the first page of the library +- **last** (required): the URL of the last page of the library +- **summary** (optional): a description for the library + +.. note:: + + Crawling library pages requires authentication and an approved follow, unless the library is + public. + +.. _object-audio: + +Audio +^^^^^ + +.. note:: + + This object `is specified in ActivityStreams `_, + but Funkwhale needs non-standard attributes to handle it. + +Example +******* + +.. code-block:: json + + { + "type": "Audio", + "id": "https://awesome.music/federation/music/uploads/88f0bc20-d7fd-461d-a641-dd9ac485e096", + "name": "For Whom the Bell Tolls - Ride the Lightning - Metallica", + "size": 8656581, + "bitrate": 320000, + "duration": 213, + "library": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6", + "updated": "2018-10-02T19:49:35.646372+00:00", + "published": "2018-10-02T19:49:35.646359+00:00", + "track": {}, + "url": { + "href": "https://awesome.music/api/v1/listen/82ece296-6397-4e26-be90-bac5f9990240/?upload=88f0bc20-d7fd-461d-a641-dd9ac485e096", + "type": "Link", + "mediaType": "audio/mpeg" + } + } + +Structure +********* + +- **id** (required): a uri identifying the audio over federation +- **name** (required): a human-friendly title for the audio (We concatenate track name, album title and artist name) +- **size** (required, non-standard): the size of the audio, in bytes +- **bitrate** (required, non-standard): the bitrate of the audio, in bytes/s +- **duration** (required): the duration of the audio, in seconds +- **library** (required, non-standard): the id of the :ref:`object-Library` object that contains the object +- **published** (required): the publication date of the entity (on the federation) +- **updated** (required): the last update date of the entity (on the federation) +- **url** (required): a ``Link`` object with an ``audio/`` mediaType where the audio file is downloadable +- **track** (required, non-standard): the :ref:`object-track` the :ref:`object-audio` is bound to + +.. note:: + + Accessing the Audio file via its url requires authentication and an approved follow on the upload library, + unless the library is public. + + +.. _library-access: + +Audio fetching on restricted libraries +-------------------------------------- + +:ref:`object-library` and :ref:`object-audio` url objects may require additional authentications +to be accessed. + +For :ref:`object-library` objects: + +- If the library is public, library pages can be accessed without restriction +- Otherwise, the HTTP request must be signed by an actor with an approved follow on the library + + +For :ref:`object-audio` url objects: + +- If the audio's library is public, audio file can be accessed without restriction +- Otherwise, the HTTP request must be signed by an actor with an approved follow on the audio's library diff --git a/docs/index.rst b/docs/index.rst index b107739ae..ef02db64b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,11 +15,11 @@ Funkwhale is a self-hosted, modern free and open-source music server, heavily in features architecture installation/index - upgrading + upgrading/index configuration troubleshooting importing-music - federation + federation/index api third-party contributing diff --git a/docs/installation/debian.rst b/docs/installation/debian.rst index 407f71dd7..1ce9d4a9e 100644 --- a/docs/installation/debian.rst +++ b/docs/installation/debian.rst @@ -116,6 +116,10 @@ Then we'll download the frontend files: git clone https://code.eliotberriot.com/funkwhale/funkwhale funkwhale cd funkwhale + By default, the repository will use the ``develop`` which may be unstable thus not recommended for production instances (unless you know what your doing). You should use the master branch instead: + + git checkout master + You'll also need to re-create the folders we make earlier: mkdir -p config data/static data/media data/music front @@ -207,7 +211,7 @@ Download the sample environment file: .. parsed-literal:: - curl -L -o config/.env "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/env.prod.sample" + curl -L -o config/.env "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/env.prod.sample" .. note:: diff --git a/docs/installation/docker.rst b/docs/installation/docker.rst index 065cfd27e..8b594eaa3 100644 --- a/docs/installation/docker.rst +++ b/docs/installation/docker.rst @@ -9,16 +9,18 @@ Download the sample docker-compose file: .. parsed-literal:: - mkdir -p /srv/funkwhale cd /srv/funkwhale - curl -L -o docker-compose.yml "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/docker-compose.yml" + mkdir nginx + curl -L -o nginx/funkwhale.template "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/docker.nginx.template" + curl -L -o nginx/funkwhale_proxy.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/funkwhale_proxy.conf" + curl -L -o docker-compose.yml "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/docker-compose.yml" Create your env file: .. parsed-literal:: export FUNKWHALE_VERSION="|version|" - curl -L -o .env "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/env.prod.sample" + curl -L -o .env "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/env.prod.sample" sed -i "s/FUNKWHALE_VERSION=latest/FUNKWHALE_VERSION=$FUNKWHALE_VERSION/" .env Ensure to edit it to match your needs (this file is heavily commented) diff --git a/docs/installation/external_dependencies.rst b/docs/installation/external_dependencies.rst index 981997207..6156ed088 100644 --- a/docs/installation/external_dependencies.rst +++ b/docs/installation/external_dependencies.rst @@ -91,3 +91,8 @@ On Arch Linux and its derivatives: sudo pacman -S redis This should be enough to have your redis server set up. + +External Authentication (LDAP) +---------------------- + +LDAP support requires some additional dependencies to enable. On the OS level both ``libldap2-dev`` and ``libsasl2-dev`` are required, and the Python modules ``python-ldap`` and ``django-auth-ldap`` must be installed. These dependencies are all included in the ``requirements.*`` files so deploying with those will install these dependencies by default. However, they are not required unless LDAP support is explicitly enabled. See :doc:`./ldap` for more details. diff --git a/docs/installation/index.rst b/docs/installation/index.rst index aba6d55a2..bba58315e 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -11,6 +11,12 @@ The project relies on the following components and services to work: - A redis server to store cache and tasks data - A celery worker to run asynchronouse tasks (such as music import) - A celery scheduler to run recurrent tasks +- A `ntp-synced clock `_ to ensure federation is working seamlessly + +.. note:: + + The synced clock is needed for federation purpose, to assess + the validity of incoming requests. Hardware requirements @@ -66,12 +72,31 @@ We also maintain an installation guide for Debian 9 and Arch Linux. docker systemd - Funkwhale packages are available for the following platforms: - `YunoHost 3 `_: https://github.com/YunoHost-Apps/funkwhale_ynh (kindly maintained by `@Jibec `_) +Running Funkwhale on the develop branch +--------------------------------------- + +Traditionnal deployments are done using specific releases. However, you +may want to benefits from the latest change available, or the help detect +bugs before they are included in actual releases. + +To do that, you'll need to run your instance on the develop branch, +which contains all the unreleased changes and features of the next version. + +Please take into account that the develop branch +may be unstable and will contain bugs that may affect the well being of your +instance. If you are comfortable with that, you need to backup at least your database +before pulling latest changes from the develop branch. + +Otherwise, the deployment process is similar to deploying with releases. +You simply need to use ``export FUNKWHALE_VERSION=develop`` +in the installation and upgrade process instead of a real version number, +as we build artifacts on the development branch the same way we do for releases. + .. _frontend-setup: Frontend setup @@ -114,16 +139,61 @@ On Arch Linux and its derivatives: sudo pacman -S nginx -Then, download our sample virtualhost file and proxy conf: +To avoid configuration errors at this level, we will generate an nginx configuration +using your .env file. This will ensure your reverse-proxy configuration always +match the application configuration and make upgrade/maintenance easier. -.. parsed-literal:: +On docker deployments, run the following commands: - curl -L -o /etc/nginx/funkwhale_proxy.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/funkwhale_proxy.conf" - curl -L -o /etc/nginx/sites-available/funkwhale.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/|version|/deploy/nginx.conf" +.. code:: shell + + # download the needed files + curl -L -o /etc/nginx/funkwhale_proxy.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/funkwhale_proxy.conf" + curl -L -o /etc/nginx/sites-available/funkwhale.template "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/docker.proxy.template" + + # create a final nginx configuration using the template based on your environment + set -a && source /srv/funkwhale/.env && set +a + envsubst "`env | awk -F = '{printf \" $%s\", $$1}'`" \ + < /etc/nginx/sites-available/funkwhale.template \ + > /etc/nginx/sites-available/funkwhale.conf + ln -s /etc/nginx/sites-available/funkwhale.conf /etc/nginx/sites-enabled/ -Ensure static assets and proxy pass match your configuration, and check the configuration is valid with ``nginx -t``. -If everything is fine, you can restart your nginx server with ``service nginx restart``. +On non-docker deployments, run the following commands: + +.. code:: shell + + # download the needed files + curl -L -o /etc/nginx/funkwhale_proxy.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/funkwhale_proxy.conf" + curl -L -o /etc/nginx/sites-available/funkwhale.template "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/nginx.template" + + # create a final nginx configuration using the template based on your environment + set -a && source /srv/funkwhale/config/.env && set +a + envsubst "`env | awk -F = '{printf \" $%s\", $$1}'`" \ + < /etc/nginx/sites-available/funkwhale.template \ + > /etc/nginx/sites-available/funkwhale.conf + + ln -s /etc/nginx/sites-available/funkwhale.conf /etc/nginx/sites-enabled/ + +.. note:: + + The resulting file should not contain any variable such as ``${FUNKWHALE_HOSTNAME}``. + You can check that using this command:: + + grep '${' /etc/nginx/sites-available/funkwhale.conf + +.. note:: + + You can freely adapt the resulting file to your own needs, as we cannot + cover every use case with a single template, especially when it's related + to SSL configuration. + +Finally, enable the resulting configuration: + +.. code-block:: bash + ln -s /etc/nginx/sites-available/funkwhale.conf /etc/nginx/sites-enabled/ + +Check the configuration is valid with ``nginx -t`` then reload your nginx server with ``systemctl restart nginx``. .. warning:: diff --git a/docs/installation/ldap.rst b/docs/installation/ldap.rst new file mode 100644 index 000000000..d38ba87a1 --- /dev/null +++ b/docs/installation/ldap.rst @@ -0,0 +1,42 @@ +LDAP configuration +================== + +LDAP is a protocol for providing directory services, in practice allowing a central authority for user login information. + +Funkwhale supports LDAP through the Django LDAP authentication module and by setting several configuration options. + +.. warning:: + + Note the following restrictions when using LDAP: + + * LDAP-based users cannot change passwords inside the app. + +Dependencies +------------ + +LDAP support requires some additional dependencies to enable. On the OS level both ``libldap2-dev`` and ``libsasl2-dev`` are required, and the Python modules ``python-ldap`` and ``django-auth-ldap`` must be installed. These dependencies are all included in the ``requirements.*`` files so deploying with those will install these dependencies by default. However, they are not required unless LDAP support is explicitly enabled. + +Environment variables +--------------------- + +LDAP authentication is configured entirely through the environment variables. The following options enable the LDAP features: + +Basic features: + +* ``LDAP_ENABLED``: Set to ``True`` to enable LDAP support. Default: ``False``. +* ``LDAP_SERVER_URI``: LDAP URI to the authentication server, e.g. ``ldap://my.host:389``. +* ``LDAP_BIND_DN``: LDAP user DN to bind as to perform searches. +* ``LDAP_BIND_PASSWORD``: LDAP user password for bind DN. +* ``LDAP_SEARCH_FILTER``: The LDAP user filter, using ``{0}`` as the username placeholder, e.g. ``(|(cn={0})(mail={0}))``; uses standard LDAP search syntax. Default: ``(uid={0})``. +* ``LDAP_START_TLS``: Set to ``True`` to enable LDAP StartTLS support. Default: ``False``. +* ``LDAP_ROOT_DN``: The LDAP search root DN, e.g. ``dc=my,dc=domain,dc=com``; supports multiple entries in a space-delimited list, e.g. ``dc=users,dc=domain,dc=com dc=admins,dc=domain,dc=com``. +* ``LDAP_USER_ATTR_MAP``: A mapping of Django user attributes to LDAP values, e.g. ``first_name:givenName, last_name:sn, username:cn, email:mail``. Default: ``first_name:givenName, last_name:sn, username:cn, email:mail``. + +Group features: + +For details on these options, see `the Django documentation `_. Group configuration is disabled unless an ``LDAP_GROUP_DN`` is set. This is an advanced LDAP feature and most users should not need to configure these settings. + +* ``LDAP_GROUP_DN``: The LDAP group search root DN, e.g. ``ou=groups,dc=domain,dc=com``. +* ``LDAP_GROUP_FILTER``: The LDAP group filter, e.g. ``(objectClass=groupOfNames)``. +* ``LDAP_REQUIRE_GROUP``: A group users must be a part of to authenticate, e.g. ``cn=enabled,ou=groups,dc=domain,dc=com``. +* ``LDAP_DENY_GROUP``: A group users must not be a part of to authenticate, e.g. ``cn=disabled,ou=groups,dc=domain,dc=com``. diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 61fa4c68a..323bb6443 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -202,7 +202,7 @@ similar issues before doing that, and use the issue tracker only to report bugs, If you ever need to share screenshots or urls with someone else, ensure those do not include your personnal token. This token is binded to your account and can be used to connect and use your account. - Urls that includes your token looks like: ``https://your.instance/api/v1/trackfiles/42/serve/?jwt=yoursecrettoken`` + Urls that includes your token looks like: ``https://your.instance/api/v1/uploads/42/serve/?jwt=yoursecrettoken`` Improving this documentation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/upgrading/0.17.rst b/docs/upgrading/0.17.rst new file mode 100644 index 000000000..1414b563e --- /dev/null +++ b/docs/upgrading/0.17.rst @@ -0,0 +1,214 @@ +About Funkwhale 0.17 +==================== + +Funkwhale 0.17 is a special version, which contains a lot of breaking changes. + +Before doing the upgrade, please read this document carefully. + + +Overview of the changes +^^^^^^^^^^^^^^^^^^^^^^^ + + +.. note:: + + The what and why are described more thoroughly in this page: https://code.eliotberriot.com/funkwhale/funkwhale/merge_requests/368 + +To sum it up, this release big completely changes the way audio content is managed in Funkwhale. +As you may guess, this has a huge impact on the whole project, because audio is at the +core of Funkwhale. + +Here is a side by side comparison of earlier versions and this release +to help you understand the scale of the changes: + ++----------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Before | After | Reason | ++========================================================================================+=================================================================================================+=========================================================================================================================================================================================================================================================+ +| There is one big audio library, managed at the instance level | Each user can have their own libraries (either public, private or shared at the instance level) | Managing the library at instance was cumbersome and dangerous: sharing an instance library over federation would quickly pose copyright issues, as well as opening public instances. It also made it impossible to only share a subset of the music. | ++----------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Users needed a specific permissions from instance owners to upload audio content | Users can upload music to their own libraries without any specific permissions | This change makes it easier for new users to start using Funkwhale, and for creators to share their content on the network. | ++----------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Users with permissions can upload as much content as they want in the instance library | Users have a storage quota and cannot exceed that storage | This change gives visibiliy to instance owners about their resource usage. If you host 100 users with a 1Gb quota, you know that your Funkwhale instance will not store more than 100Gb of music files. | ++----------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| N/A | Users can upload private content or share content with only specific users | This is a new feature, and we think it will enable users to upload their own music libraries to their instance, without breaking the law or putting their admins in trouble, since their media will remain private. | ++----------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Youtube Import | This feature is removed | This feature posed copyright issues and impacted the credibility of the project, so we removed it. | ++----------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Music requests | This feature is removed | Since all users can now upload content without specific permissions, we think this feature is less-likely to be useful in its current state. | ++----------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +From a shared, instance-wide library to users libraries +------------------------------------------------------ + +As you can see, there is a big switch: in earlier versions, each instance had one big library, +that was available to all its users. This model don't scale well (especially if you put +federation on top of that), because it's an all-or-nothing choice if you want to share it. + +Starting from version 0.17, each user will be able to create personal libraries +and upload content in those, up to a configurable quota. + +Those libraries can have one of the following visibility level: + +- **Private**: only the owner of the library can access its content +- **Instance**: users from the same instance can access the library content +- **Public**: everyone (including other instances) can access the library content + +Regardless of this visibility level, library owners can also share them manually +with other users, both from the same instance or from the federation. + +We think this change will have a really positive impact: + +- Admins should be more encline to open their instance to strangers, because copyrighted media + can be upload and shared privately +- Creators should have a better experience when joining the network, because they can now + upload their own content and share it over the federation without any admin intervention +- The federation should grow faster, because user libraries can contain copyrighted content + and be shared, without putting the admins at risk + +Accessing music +--------------- + +From an end-user perspective, you will be able to browse any artist or album or track +that is known by your instance, but you'll only be able to listen to content +that match one of those critaeria: + +- The content is available is one of your libraries +- The content is available in a public library +- The content is available in one library from your instance that has a visibility level set to "instance" +- The content is available in one of the library you follow + +Following someone else's library is a four step process: + +1. Get the library link from its owner +2. Use this link on your instance to follow the library +3. Wait until your follow request is approved by the library owner +4. If this library is unknown on your instance, it will be scanned to import its content, which may take a few minutes + +Libraries owner can revoke follows at any time, which will effectively prevent +the ancient follower from accessing the library content. + +A brand new federation +---------------------- + +This is more "under the hood" work, but the whole federation/ActivityPub logic +was rewritten for this release. This new implementation is more spec compliant +and should scale better. + +The following activities are propagated over federation: + +- Library follow creation, accept and reject +- Audio creation and deletion +- Library deletion + +A better import UI +------------------ + +This version includes a completely new import UI which should make +file uploading less annoying. Especially it's updating in real-time +and has a better error reporting. + +A Better import engine +---------------------- + +Funkwhale is known for its quircks during music import. Missing covers, +splitted albums, bad management of tracks with multiple artists, missing +data for files imported over federation, bad performance, discrepencies between +the user provided tags and what is actually stored in the database... + +This should be greatly improved now, as the whole import logic was rewritten +from scratch. + +Import is done completely offline and do not call the MusicBrainz API anymore, +except to retrieve covers if those are not embedded in the imported files. +MusicBrainzare references are still stored in the database, but we rely solely +on the tags from the audio file now. + +This has two positive consequences: + +- Improved performance for both small and big imports (possibly by a factor 10) +- More reliable import result: if your file is tagged in a specific way, we will only + use tags for the import. + +Imports from federation, command-line and UI/API all use the same code, +which should greatly reduce the bugs/discrepencies. + +Finally, the import engine now understand the difference between a track artist +and an album artist, which should put an end to the album splitting issues +for tracks that had a different artist than the album artist. + +What will break +--------------- + +If you've read until here, you can probably understand that all of these changes +comes at a cost: version 0.17 contains breaking changes, feature were removed +or changed. + +Those features were removed: + +- YouTube imports: for copyright reasons, keeping this in the core was not possible +- Music requests: those are now less useful since anyone can upload content + +Also, the current federation will break, as it's absolutely not compatible +with what we've built in version 0.17, and maintaining compatibility was simply not possible. + +Apart from that, other features should work the same way as they did before. + +Migration path +-------------- + +.. warning:: + + This migration is huge. Do a backup. Please. The database, and the music files. + Please. + +.. warning:: I'm not kidding. + + +Migration will be similar to previous ones, with an additional script to run that will +take care of updating existing rows in the database. Especially, this script +will be responsible to create a library for each registered user, and to +bind content imported by each one to this library. + +Libraries created this way will have a different visibility level depending of your instance configuration: + +- If your instance requires authentication to access the API / Listen to music, libraries will + be marked with "instance" visibility. As a result, all users from the instance will still + be able to listen to all the music of the instance after the migration +- If your instance does not requires authentication to access the API / Listen to music, + libraries will be completely public, allowing anyone to access the content (including federation) + +This script will contain other database-related operations, but the impact will remain +invisible. + + +Upgrade instructions +-------------------- + +Follow instructions from https://docs.funkwhale.audio/upgrading/index.html, +then run the migrations script. + +On docker-setups:: + + # if you missed this one from a previous upgrade + docker-compose run --rm api python manage.py script create_actors --no-input + docker-compose run --rm api python manage.py script migrate_to_user_libraries --no-input + +On non docker-setups:: + + # if you missed this one from a previous upgrade + python api/manage.py script create_actors --no-input + python api/manage.py script migrate_to_user_libraries --no-input + +If the scripts ends without errors, you're instance should be updated and ready to use :) + +.. note:: + + If you use nginx, ensure your funkwhale_proxy.conf file does not contain this: + + proxy_set_header X-Forwarded-Host $host:$server_port; + + If you have this line present, replace it with: + + proxy_set_header X-Forwarded-Host $host; + + And reload your nginx server. diff --git a/docs/upgrading.rst b/docs/upgrading/index.rst similarity index 87% rename from docs/upgrading.rst rename to docs/upgrading/index.rst index 49ea55881..08c8a5689 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading/index.rst @@ -18,6 +18,18 @@ similarly from version to version, but some of them may require additional steps Those steps would be described in the version release notes. +Insights about new versions +--------------------------- + +Some versions may be bigger than usual, and we'll try to detail the changes +when possible. + +.. toctree:: + :maxdepth: 1 + + 0.17 + + Docker setup ------------ @@ -30,6 +42,11 @@ easy: # hardcode the targeted version your env file # (look for the FUNKWHALE_VERSION variable) nano .env + # Load your environment variables + source .env + # Download newest nginx configuration file + curl -L -o nginx/funkwhale.template "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/docker.nginx.template" + curl -L -o nginx/funkwhale_proxy.conf "https://code.eliotberriot.com/funkwhale/funkwhale/raw/develop/deploy/funkwhale_proxy.conf" # Pull the new version containers docker-compose pull # Apply the database migrations diff --git a/front/locales/app.pot b/front/locales/app.pot index 9eb4a2a7d..441c1cc35 100644 --- a/front/locales/app.pot +++ b/front/locales/app.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: front 1.0.0\n" +"Project-Id-Version: front 0.1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-22 14:12+0200\n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -30,14 +30,15 @@ msgstr "" msgid "(empty)" msgstr "" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "" msgstr[1] "" -#: front/src/components/Sidebar.vue:116 src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" msgstr[0] "" @@ -49,7 +50,7 @@ msgid_plural "%{ count } tracks in %{ albumsCount } albums" msgstr[0] "" msgstr[1] "" -#: front/src/components/library/radios/Builder.vue:76 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" msgstr[0] "" @@ -61,6 +62,10 @@ msgid_plural "%{ count } tracks" msgstr[0] "" msgstr[1] "" +#: front/src/views/content/libraries/Quota.vue:11 +msgid "%{ current } used on %{ max } allowed" +msgstr "" + #: front/src/components/common/Duration.vue:2 msgid "%{ hours } h %{ minutes } min" msgstr "" @@ -77,10 +82,6 @@ msgstr "" msgid "%{ user } listened to a track" msgstr "" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" @@ -93,35 +94,27 @@ msgid_plural "%{ count } favorites" msgstr[0] "" msgstr[1] "" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "" -msgstr[1] "" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 +#: front/src/views/content/libraries/Detail.vue:48 +msgid "Accept" +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:40 msgid "Accepted" msgstr "" -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "" - -#: front/src/components/Home.vue:109 +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" msgstr "" @@ -133,10 +126,6 @@ msgstr "" msgid "Account settings" msgstr "" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "" @@ -145,16 +134,18 @@ msgstr "" msgid "Account's email" msgstr "" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +msgid "Action" +msgstr "" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" msgstr[0] "" msgstr[1] "" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:60 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "" @@ -162,30 +153,22 @@ msgstr "" msgid "Active" msgstr "" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" msgstr "" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" msgstr "" -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" msgstr "" -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "" - #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" @@ -200,10 +183,6 @@ msgstr "" msgid "Add to queue" msgstr "" -#: front/src/components/playlists/PlaylistModal.vue:116 -msgid "Add to this playlist" -msgstr "" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "" @@ -212,24 +191,18 @@ msgstr "" msgid "Admin" msgstr "" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" msgstr "" -#: front/src/components/audio/SearchBar.vue:26 src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "" -msgstr[1] "" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" @@ -241,7 +214,6 @@ msgid "Album page" msgstr "" #: front/src/components/audio/Search.vue:19 src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "" @@ -250,8 +222,8 @@ msgstr "" msgid "Albums by this artist" msgstr "" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "" @@ -263,44 +235,19 @@ msgstr "" msgid "An unknown error happend, this can mean the server is down or cannot be reached" msgstr "" -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "" - #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "" -#: front/src/components/audio/SearchBar.vue:25 src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" msgstr "" -#: front/src/components/requests/Form.vue:5 +#: front/src/components/requests/Form.vue:5 src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "" @@ -308,22 +255,17 @@ msgstr "" msgid "Artist page" msgstr "" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "" - #: front/src/components/audio/Search.vue:10 src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 src/components/library/Library.vue:7 +#: front/src/components/library/Library.vue:7 src/components/library/Artists.vue:120 msgid "Artists" msgstr "" -#: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 src/components/library/Radios.vue:44 +#: front/src/components/favorites/List.vue:33 src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" msgstr "" @@ -331,10 +273,6 @@ msgstr "" msgid "Ask for a password reset" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "" - #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" msgstr "" @@ -348,12 +286,9 @@ msgstr "" msgid "Back to login" msgstr "" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "" - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "" @@ -361,7 +296,7 @@ msgstr "" msgid "Browse" msgstr "" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "" @@ -369,18 +304,6 @@ msgstr "" msgid "Browsing artists" msgstr "" -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "" - #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" msgstr "" @@ -397,22 +320,17 @@ msgstr "" msgid "By %{ artist }" msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "" - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" msgstr "" -#: front/src/components/library/radios/Builder.vue:59 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "" @@ -420,7 +338,7 @@ msgstr "" msgid "Cannot change your password" msgstr "" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" msgstr "" @@ -453,7 +371,7 @@ msgstr "" msgid "Changing your password will have the following consequences" msgstr "" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "" @@ -470,21 +388,12 @@ msgstr "" msgid "Clear playlist" msgstr "" -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "" - #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" msgstr "" -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" msgstr "" #: front/src/components/manage/users/InvitationForm.vue:26 @@ -498,12 +407,11 @@ msgstr "" msgid "Collapse" msgstr "" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "" -#: front/src/components/library/radios/Builder.vue:58 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" msgstr "" @@ -520,11 +428,11 @@ msgstr "" msgid "Confirmation code" msgstr "" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" msgstr "" -#: front/src/components/Home.vue:88 +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" msgstr "" @@ -532,19 +440,26 @@ msgstr "" msgid "Create a funkwhale account" msgstr "" +#: front/src/views/content/libraries/Home.vue:14 +msgid "Create a new library" +msgstr "" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "" +#: front/src/views/content/libraries/Form.vue:26 +msgid "Create library" +msgstr "" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "" @@ -557,9 +472,8 @@ msgstr "" msgid "Create your own radio" msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "" @@ -567,17 +481,33 @@ msgstr "" msgid "Current avatar" msgstr "" +#: front/src/views/content/libraries/DetailArea.vue:4 +msgid "Current library" +msgstr "" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +msgid "Current usage" +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:29 src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "" +#: front/src/views/content/libraries/Form.vue:39 +msgid "Delete library" +msgstr "" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "" @@ -586,31 +516,30 @@ msgstr "" msgid "Delete radio" msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" +#: front/src/views/content/libraries/Form.vue:31 +msgid "Delete this library?" msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "" - -#: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 src/components/library/Radios.vue:47 +#: front/src/components/favorites/List.vue:34 src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +msgid "Description" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +#: front/src/views/content/remote/Card.vue:50 +msgid "Details" msgstr "" #: front/src/components/auth/Settings.vue:104 @@ -630,7 +559,7 @@ msgstr "" msgid "Discover how to use Funkwhale from other apps" msgstr "" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "" @@ -656,40 +585,42 @@ msgid_plural "Do you want to launch %{ action } on %{ count } elements?" msgstr[0] "" msgstr[1] "" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "" -#: front/src/components/audio/track/Table.vue:24 src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" msgstr "" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "" +#: front/src/views/content/libraries/Detail.vue:9 +msgid "Edit" +msgstr "" + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "" @@ -710,55 +641,16 @@ msgstr "" msgid "End edition" msgstr "" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "" - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "" - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "" - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "" - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "" - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "" - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" +#: front/src/views/content/remote/Card.vue:39 +msgid "Error during scan" msgstr "" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "" @@ -778,29 +670,28 @@ msgstr "" msgid "Error while creating invitation" msgstr "" +#: front/src/views/content/remote/ScanForm.vue:3 +msgid "Error while fetching remote library" +msgstr "" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" +#: front/src/views/content/libraries/Quota.vue:75 +msgid "Errored files" msgstr "" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" +#: front/src/views/content/remote/Card.vue:58 +msgid "Errored tracks:" msgstr "" -#: front/src/components/library/radios/Builder.vue:57 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "" @@ -809,6 +700,7 @@ msgid "Expand" msgstr "" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "" @@ -820,92 +712,48 @@ msgstr "" msgid "Expired/used" msgstr "" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "" - -#: front/src/components/Sidebar.vue:87 src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "" - #: front/src/views/admin/library/Base.vue:5 src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "" - -#: front/src/components/library/radios/Builder.vue:56 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" +#: front/src/views/content/remote/Card.vue:88 +msgid "Follow pending approval" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" msgstr "" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "" -#: front/src/components/activity/Like.vue:12 src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "" @@ -913,7 +761,7 @@ msgstr "" msgid "From album %{ album } by %{ artist }" msgstr "" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" msgstr "" @@ -921,7 +769,7 @@ msgstr "" msgid "Funkwhale is compatible with other music players that support the Subsonic API." msgstr "" -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "" @@ -929,7 +777,7 @@ msgstr "" msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." msgstr "" -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." msgstr "" @@ -945,12 +793,12 @@ msgstr "" msgid "Get me to the library" msgstr "" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " +#: front/src/components/Home.vue:76 +msgid "Get quality metadata about your music thanks to MusicBrainz" +msgstr "" + +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +msgid "Get started" msgstr "" #: front/src/components/common/ActionTable.vue:21 @@ -962,18 +810,10 @@ msgstr "" msgid "Go to home page" msgstr "" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "" - -#: front/src/App.vue:74 +#: front/src/App.vue:73 msgid "Help us translate Funkwhale" msgstr "" -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "" - #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" msgstr "" @@ -982,150 +822,45 @@ msgstr "" msgid "However, accessing Funkwhale from those clients require a separate password you can set below." msgstr "" -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "" - #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." msgstr "" -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "" -msgstr[1] "" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "" - #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" msgstr "" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" +#: front/src/components/library/FileUpload.vue:51 +msgid "Import reference" msgstr "" -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" -msgstr "" - -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" msgstr[0] "" msgstr[1] "" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1140,49 +875,41 @@ msgstr "" msgid "Invitations" msgstr "" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" msgstr "" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" msgstr "" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." +#: front/src/views/content/remote/Home.vue:14 +msgid "Known libraries" msgstr "" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +msgid "Last update:" +msgstr "" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" msgstr "" @@ -1190,24 +917,21 @@ msgstr "" msgid "Learn more about this instance" msgstr "" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "" -#: front/src/views/federation/Base.vue:5 src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "" + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "" @@ -1215,147 +939,109 @@ msgstr "" msgid "Library files" msgstr "" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "" +#: front/src/views/content/libraries/Detail.vue:21 +msgid "Loading followers..." +msgstr "" + +#: front/src/views/content/libraries/Home.vue:3 +msgid "Loading Libraries..." +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +msgid "Loading library data..." +msgstr "" + +#: front/src/views/Notifications.vue:4 +msgid "Loading notifications..." +msgstr "" + +#: front/src/views/content/remote/Home.vue:3 +msgid "Loading remote libraries..." +msgstr "" + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "" -#: front/src/components/favorites/List.vue:5 -msgid "Loading your favorites..." +#: front/src/views/content/libraries/Quota.vue:4 +msgid "Loading usage data..." msgstr "" -#: front/src/components/auth/Login.vue:78 -msgid "Log In" +#: front/src/components/favorites/List.vue:5 +msgid "Loading your favorites..." msgstr "" #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "" - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "" - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "" - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" +#: front/src/views/Notifications.vue:17 +msgid "Mark all as read" msgstr "" -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" msgstr "" -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." -msgstr "" - -#: front/src/components/Sidebar.vue:48 src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" +#: front/src/views/content/libraries/Home.vue:6 +msgid "My libraries" msgstr "" -#: front/src/components/library/radios/Builder.vue:227 -msgid "My awesome radio" -msgstr "" - -#: front/src/components/library/Track.vue:64 src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "" #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "" @@ -1364,48 +1050,39 @@ msgstr "" msgid "New password" msgstr "" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "" -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" msgstr "" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "" -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" +#: front/src/components/federation/LibraryWidget.vue:6 +msgid "No matching library." msgstr "" -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" msgstr "" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +msgid "Notifications" +msgstr "" + +#: front/src/App.vue:36 msgid "Official website" msgstr "" @@ -1413,41 +1090,30 @@ msgstr "" msgid "Old password" msgstr "" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "" - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 msgid "Open" msgstr "" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "" -#: front/src/components/library/import/Main.vue:93 -msgid "Or" -msgstr "" - -#: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 src/components/library/Radios.vue:33 +#: front/src/components/favorites/List.vue:23 src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" msgstr "" -#: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 src/components/library/Radios.vue:41 +#: front/src/components/favorites/List.vue:31 src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "" @@ -1455,10 +1121,6 @@ msgstr "" msgid "Owner" msgstr "" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "" @@ -1467,42 +1129,25 @@ msgstr "" msgid "Password" msgstr "" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" msgstr "" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" +#: front/src/views/content/libraries/Quota.vue:22 +msgid "Pending files" msgstr "" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "" @@ -1525,10 +1170,6 @@ msgstr "" msgid "Play all albums" msgstr "" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "" @@ -1537,14 +1178,6 @@ msgstr "" msgid "Play now" msgstr "" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1571,9 +1204,9 @@ msgstr "" msgid "Playlist visibility" msgstr "" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" msgstr "" @@ -1593,12 +1226,8 @@ msgstr "" msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." msgstr "" -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" +#: front/src/components/library/FileUpload.vue:58 +msgid "Proceed" msgstr "" #: front/src/views/auth/EmailConfirm.vue:26 @@ -1606,30 +1235,35 @@ msgstr "" msgid "Proceed to login" msgstr "" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" msgstr "" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" msgstr "" #: front/src/components/Sidebar.vue:20 msgid "Queue" msgstr "" -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "" - -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "" - -#: front/src/components/library/radios/Builder.vue:226 -msgid "Radio Builder" -msgstr "" - #: front/src/components/library/radios/Builder.vue:15 msgid "Radio created" msgstr "" @@ -1642,7 +1276,7 @@ msgstr "" msgid "Radio updated" msgstr "" -#: front/src/components/library/Library.vue:10 src/components/library/Radios.vue:141 +#: front/src/components/library/Library.vue:10 src/components/library/Radios.vue:142 msgid "Radios" msgstr "" @@ -1662,19 +1296,11 @@ msgstr "" msgid "Recently listened" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "" - #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" msgstr "" @@ -1687,6 +1313,22 @@ msgstr "" msgid "regular user" msgstr "" +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "" + +#: front/src/views/content/remote/Home.vue:6 +msgid "Remote libraries" +msgstr "" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." +msgstr "" + #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" msgstr "" @@ -1695,10 +1337,6 @@ msgstr "" msgid "Remove avatar" msgstr "" -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "" - #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" @@ -1712,104 +1350,57 @@ msgstr "" msgid "Request a password" msgstr "" -#: front/src/App.vue:35 -msgid "Request music" -msgstr "" - -#: front/src/views/library/MusicRequest.vue:4 src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "" - #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "" - #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" msgstr "" -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "" - #: front/src/components/favorites/List.vue:38 src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" msgstr "" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:29 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" +#: front/src/views/content/remote/Card.vue:31 +msgid "Scan pending" +msgstr "" + +#: front/src/views/content/remote/Card.vue:43 +msgid "Scanned successfully" +msgstr "" + +#: front/src/views/content/remote/Card.vue:47 +msgid "Scanned with errors" +msgstr "" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" msgstr "" -#: front/src/components/federation/LibraryTrackTable.vue:5 #: front/src/components/library/Artists.vue:10 src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 src/views/playlists/List.vue:13 +#: front/src/views/content/libraries/FilesTable.vue:5 src/views/playlists/List.vue:13 msgid "Search" msgstr "" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "" - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "" - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "" - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "" - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "" - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." +#: front/src/views/content/remote/ScanForm.vue:9 +msgid "Search a remote library" msgstr "" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" msgstr "" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" msgstr "" @@ -1818,42 +1409,27 @@ msgstr "" msgid "Search on Wikipedia" msgstr "" -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "" - #: front/src/views/admin/Settings.vue:15 msgid "Sections" msgstr "" -#: front/src/components/library/radios/Builder.vue:41 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" msgstr "" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" msgstr[0] "" msgstr[1] "" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "" - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" msgstr "" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "" - -#: front/src/components/Sidebar.vue:97 src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" msgstr "" @@ -1865,69 +1441,63 @@ msgstr "" msgid "Settings updated successfully." msgstr "" -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "" - #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" msgstr "" +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +msgid "Sharing link" +msgstr "" + +#: front/src/components/audio/album/Card.vue:40 +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "" +msgstr[1] "" + #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" msgstr[0] "" msgstr[1] "" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "" -msgstr[1] "" - -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" +#: front/src/views/Notifications.vue:10 +msgid "Show read notifications" msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" msgstr "" -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "" - #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" msgstr "" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "" - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 msgid "Skipped" msgstr "" +#: front/src/views/content/libraries/Quota.vue:49 +msgid "Skipped files" +msgstr "" + #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "" @@ -1940,21 +1510,15 @@ msgstr "" msgid "Sorry, we did not found any artist matching your query" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" msgstr "" @@ -1963,26 +1527,11 @@ msgstr "" msgid "Start" msgstr "" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "" @@ -1990,15 +1539,11 @@ msgstr "" msgid "Stop" msgstr "" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" msgstr "" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" msgstr "" @@ -2006,46 +1551,43 @@ msgstr "" msgid "Submit another request" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "" - #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" +#: front/src/App.vue:13 +msgid "Suggested choices" msgstr "" -#: front/src/App.vue:11 -msgid "Suggested choices" +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" msgstr "" #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "" +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." msgstr "" -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "" - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." msgstr "" -#: front/src/components/Home.vue:124 +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." +msgstr "" + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "" + +#: front/src/components/Home.vue:121 msgid "The plaform is free and open-source, you can install it and modify it without worries" msgstr "" @@ -2053,32 +1595,36 @@ msgstr "" msgid "The Subsonic API is not available on this Funkwhale instance." msgstr "" -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" msgstr "" -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" msgstr "" -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." msgstr "" #: front/src/components/auth/Profile.vue:16 msgid "This is you!" msgstr "" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." msgstr "" -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "" + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" msgstr "" #: front/src/views/playlists/Detail.vue:37 @@ -2093,9 +1639,142 @@ msgstr "" msgid "This will completely disable access to the Subsonic API using from account." msgstr "" -#: front/src/App.vue:137 +#: front/src/App.vue:162 src/components/About.vue:55 src/components/Home.vue:154 +#: front/src/components/PageNotFound.vue:33 src/components/Sidebar.vue:203 +#: front/src/components/Sidebar.vue:204 src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 src/components/audio/SearchBar.vue:27 +#: front/src/components/auth/Login.vue:77 src/components/auth/Login.vue:78 +#: front/src/components/auth/Logout.vue:20 src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 src/views/auth/EmailConfirm.vue:51 +#: front/src/views/auth/PasswordReset.vue:52 src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 src/views/content/Base.vue:19 +#: front/src/views/content/Home.vue:35 src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 src/views/instance/Timeline.vue:57 +#: front/src/views/playlists/Detail.vue:90 src/views/playlists/List.vue:104 +#: front/src/views/playlists/List.vue:105 src/views/radios/Detail.vue:80 msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "" +msgstr[1] "" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." @@ -2105,16 +1784,23 @@ msgstr "" msgid "This will remove all tracks from this playlist and cannot be undone." msgstr "" -#: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 -#: front/src/components/manage/library/FilesTable.vue:37 -msgid "Title" +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." msgstr "" -#: front/src/components/audio/SearchBar.vue:27 src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/components/audio/track/Table.vue:6 +#: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 +msgid "Title" msgstr "" #: front/src/components/library/Track.vue:53 @@ -2130,16 +1816,12 @@ msgid "tracks" msgstr "" #: front/src/components/library/Album.vue:43 -#: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 +#: front/src/components/playlists/PlaylistModal.vue:33 src/views/content/Base.vue:8 +#: front/src/views/content/libraries/Detail.vue:8 src/views/playlists/Detail.vue:50 #: front/src/views/radios/Detail.vue:34 msgid "Tracks" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "" - #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" msgstr "" @@ -2152,34 +1834,35 @@ msgstr "" msgid "tracks listened" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "" - +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" msgstr "" +#: front/src/views/content/remote/Card.vue:100 src/views/content/remote/Card.vue:105 +msgid "Unfollow" +msgstr "" + +#: front/src/views/content/remote/Card.vue:101 +msgid "Unfollow this library?" +msgstr "" + #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." msgstr "" -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "" - #: front/src/components/Home.vue:37 msgid "Unlimited music" msgstr "" -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "" - #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" msgstr "" +#: front/src/views/content/libraries/Form.vue:25 +msgid "Update library" +msgstr "" + #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" msgstr "" @@ -2192,8 +1875,10 @@ msgstr "" msgid "Update your password" msgstr "" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" msgstr "" @@ -2201,39 +1886,51 @@ msgstr "" msgid "Upload a new avatar" msgstr "" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" +#: front/src/views/content/Home.vue:6 +msgid "Upload audio content" msgstr "" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/views/content/libraries/FilesTable.vue:54 +msgid "Upload date" +msgstr "" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:31 +msgid "Upload new tracks" +msgstr "" + +#: front/src/views/admin/users/UsersDetail.vue:82 +msgid "Upload quota" +msgstr "" + +#: front/src/components/library/FileUpload.vue:99 +msgid "Uploaded" +msgstr "" + +#: front/src/components/library/FileUpload.vue:5 +msgid "Uploading" +msgstr "" + +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "" -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" msgstr "" -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "" - #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." msgstr "" -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "" - #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" msgstr "" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" msgstr "" @@ -2241,12 +1938,18 @@ msgstr "" msgid "User activity" msgstr "" +#: front/src/components/library/Album.vue:49 src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +msgid "User libraries" +msgstr "" + #: front/src/components/library/Radios.vue:20 msgid "User radios" msgstr "" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" msgstr "" @@ -2258,20 +1961,29 @@ msgstr "" msgid "users" msgstr "" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 src/views/admin/users/UsersList.vue:21 +#: front/src/views/admin/Settings.vue:81 msgid "Users" msgstr "" +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +msgid "View files" +msgstr "" + #: front/src/components/library/Album.vue:37 src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" msgstr "" +#: front/src/views/content/libraries/Form.vue:18 +msgid "Visibility" +msgstr "" + #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" msgstr "" @@ -2296,12 +2008,20 @@ msgstr "" msgid "We cannot save your settings" msgstr "" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" msgstr "" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:40 +msgid "we recommend using Picard for that purpose" msgstr "" #: front/src/components/Home.vue:7 @@ -2316,27 +2036,15 @@ msgstr "" msgid "We've received your request, you'll get some groove soon ;)" msgstr "" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" msgstr "" -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "" - #: front/src/components/Home.vue:24 msgid "Why funkwhale?" msgstr "" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" msgstr "" @@ -2344,15 +2052,19 @@ msgstr "" msgid "Yes, log me out!" msgstr "" +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" +msgstr "" + #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" msgstr "" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." msgstr "" -#: front/src/components/Home.vue:136 +#: front/src/components/Home.vue:133 msgid "You can invite friends and family to your instance so they can enjoy your music" msgstr "" @@ -2364,14 +2076,18 @@ msgstr "" msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." msgstr "" -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" msgstr "" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" msgstr "" +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." +msgstr "" + #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" msgstr "" @@ -2380,20 +2096,16 @@ msgstr "" msgid "You will have to update your password on your clients that use this password." msgstr "" -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "" - #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." msgstr "" -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" +#: front/src/components/Home.vue:114 +msgid "Your music, your way" msgstr "" -#: front/src/components/Home.vue:117 -msgid "Your music, your way" +#: front/src/views/Notifications.vue:7 +msgid "Your notifications" msgstr "" #: front/src/views/auth/PasswordResetConfirm.vue:29 @@ -2404,8 +2116,400 @@ msgstr "" msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" msgstr "" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +msgid "Activity visibility" +msgstr "" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "" + +#: front/src/components/mixins/Translations.vue:18 +msgid "Accessed date" +msgstr "" + +#: front/src/components/mixins/Translations.vue:19 +msgid "Modification date" +msgstr "" + +#: front/src/components/mixins/Translations.vue:20 +msgid "Imported date" +msgstr "" + +#: front/src/components/mixins/Translations.vue:22 +msgid "Track name" +msgstr "" + +#: front/src/components/mixins/Translations.vue:23 +msgid "Album name" +msgstr "" + +#: front/src/components/mixins/Translations.vue:30 +msgid "Sign-up date" +msgstr "" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "" + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "" + +#: front/src/components/library/radios/Builder.vue:236 +msgid "My awesome description" +msgstr "" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "" + +#: front/src/components/library/FileUpload.vue:240 +msgid "A network error occured while uploading this file" +msgstr "" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "" + +#: front/src/components/library/Track.vue:195 src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "" + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" msgstr[0] "" msgstr[1] "" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "" + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "" + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "" + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "" + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "" + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "" + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "" + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "" + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "" + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "" + +#: front/src/views/content/remote/ScanForm.vue:48 +msgid "Enter a library url" +msgstr "" + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:226 +msgid "Search by title, artist, album..." +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:242 +msgid "Import went on successfully" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:259 +msgid "Relaunch import" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:58 +msgid "Visibility: nobody except me" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:59 +msgid "Visibility: everyone on this instance" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:61 +msgid "Total size of the files in this library" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:70 +msgid "My awesome library" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:74 +msgid "Everyone, including other instances" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:106 +msgid "Library updated" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:109 +msgid "Library created" +msgstr "" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "" + +#: front/src/views/admin/library/Base.vue:16 +msgid "Manage library" +msgstr "" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "" + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "" + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "" diff --git a/front/locales/ar/LC_MESSAGES/app.po b/front/locales/ar/LC_MESSAGES/app.po index 20ec26881..ee528497a 100644 --- a/front/locales/ar/LC_MESSAGES/app.po +++ b/front/locales/ar/LC_MESSAGES/app.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: Arabic (FunkWhale)\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-22 14:12+0200\n" -"PO-Revision-Date: 2018-07-24 19:50+0000\n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" +"PO-Revision-Date: 2018-10-06 19:26+0000\n" "Last-Translator: ButterflyOfFire \n" "Language-Team: Arabic \n" @@ -27,8 +27,8 @@ msgstr "(%{ index } مِن %{ length })" msgid "(empty)" msgstr "(فارغ)" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "" @@ -38,14 +38,15 @@ msgstr[3] "" msgstr[4] "" msgstr[5] "" -#: front/src/components/Sidebar.vue:116 -#: src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 +#: src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" -msgstr[0] "" +msgstr[0] "%{ count } مقطع" msgstr[1] "%{ count } مَقطَع" -msgstr[2] "" -msgstr[3] "%{ count } مَقاطِع" +msgstr[2] "%{ count } مَقطعين" +msgstr[3] "%{ count } مَقطَع" msgstr[4] "%{ count } مَقاطِع" msgstr[5] "%{ count } مَقاطِع" @@ -54,12 +55,12 @@ msgid "%{ count } track in %{ albumsCount } albums" msgid_plural "%{ count } tracks in %{ albumsCount } albums" msgstr[0] "" msgstr[1] "%{ count } مَقطَع في %{ albumsCount } ألبومات" -msgstr[2] "" -msgstr[3] "" +msgstr[2] "%{ count } مَقطَع في %{ albumsCount } ألبومات" +msgstr[3] "%{ count } مَقطَع في %{ albumsCount } ألبوما" msgstr[4] "%{ count } مَقاطِع في %{ albumsCount } ألبومات" msgstr[5] "%{ count } مَقاطِع في %{ albumsCount } ألبومات" -#: front/src/components/library/radios/Builder.vue:76 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" msgstr[0] "" @@ -72,13 +73,17 @@ msgstr[5] "" #: front/src/components/playlists/Card.vue:18 msgid "%{ count} track" msgid_plural "%{ count } tracks" -msgstr[0] "" +msgstr[0] "%{ count} مَقطَع" msgstr[1] "%{ count } مَقطَع" -msgstr[2] "" -msgstr[3] "" +msgstr[2] "مقطعين" +msgstr[3] "%{ count } مَقطَع" msgstr[4] "%{ count } مَقاطِع" msgstr[5] "%{ count } مَقاطِع" +#: front/src/views/content/libraries/Quota.vue:11 +msgid "%{ current } used on %{ max } allowed" +msgstr "" + #: front/src/components/common/Duration.vue:2 msgid "%{ hours } h %{ minutes } min" msgstr "%{ hours } سا %{ minutes } د" @@ -95,63 +100,47 @@ msgstr "أُعجِب %{ user } بمقطع" msgid "%{ user } listened to a track" msgstr "قام %{ user } بالاستماع إلى مَقطَع" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "الملف الشخصي لِـ %{ username }" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" msgstr[0] "ألبوم واحد" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "" +msgstr[1] "ألبوم" +msgstr[2] "%{ count } ألبومين" +msgstr[3] "%{ count } ألبوم" msgstr[4] "%{ count } ألبومات" msgstr[5] "%{ count } ألبومات" #: front/src/components/favorites/List.vue:10 msgid "1 favorite" msgid_plural "%{ count } favorites" -msgstr[0] "مفضّلة واحدة" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "%{ count } مفضّلات" +msgstr[0] "%{ count } مفضّلة" +msgstr[1] "مفضلة" +msgstr[2] "%{ count } مفضلتين" +msgstr[3] "%{ count } مفضّلة" msgstr[4] "%{ count } مفضّلات" msgstr[5] "%{ count } مفضّلات" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "مَقطَع واحد" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "" -msgstr[4] "%{ count } مَقاطِع" -msgstr[5] "%{ count } مَقاطِع" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "عن %{ instance }" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "عن فانك وايل Funkwhale" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "عن مثيل الخادوم هذا" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 +#: front/src/views/content/libraries/Detail.vue:48 +msgid "Accept" +msgstr "تم قبوله" + +#: front/src/views/content/libraries/Detail.vue:40 msgid "Accepted" msgstr "تم قبوله" -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "عُطّل النفاذ" - -#: front/src/components/Home.vue:109 +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" msgstr "صِلوا إلى موسيقاكم عبر واجهة نظيفة التصميم تُركّز فعلًا على الأهمّ" @@ -163,10 +152,6 @@ msgstr "الحساب نشِط" msgid "Account settings" msgstr "إعدادات الحساب" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "إعدادات الحساب" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "حالة الحساب" @@ -175,7 +160,11 @@ msgstr "حالة الحساب" msgid "Account's email" msgstr "البريد الإلكتروني الخاص بالحساب" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +msgid "Action" +msgstr "الإجراء" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" msgstr[0] "" @@ -186,9 +175,7 @@ msgstr[4] "" msgstr[5] "" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:60 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "الإجراءات" @@ -196,30 +183,22 @@ msgstr "الإجراءات" msgid "Active" msgstr "النشاط" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "النشاط" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" -msgstr "" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" +msgstr "إضافة محتوى" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "إضافة مكتبة جديدة" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" msgstr "إضافة عامل تصفية" -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" msgstr "قم بإضافة عوامل تصفية لتخصيص إذاعتك" -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "إضافة إلى قائمة الانتظار الحالية" - #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" @@ -234,10 +213,6 @@ msgstr "إضافة إلى قائمة المقاطع الموسيقية …" msgid "Add to queue" msgstr "أضِف إلى قائمة الانتظار" -#: front/src/components/playlists/PlaylistModal.vue:116 -msgid "Add to this playlist" -msgstr "أضفه إلى قائمة التشغيل هذه" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "إضافة مقطع موسيقي" @@ -246,29 +221,19 @@ msgstr "إضافة مقطع موسيقي" msgid "Admin" msgstr "المدير" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" msgstr "الإدارة" -#: front/src/components/audio/SearchBar.vue:26 -#: src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 +#: src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "الألبوم" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "" -msgstr[1] "ألبوم %{ title } (%{ count } tracks) لِـ %{ artist }" -msgstr[2] "" -msgstr[3] "ألبوم %{ title } (%{ count } tracks) لِـ %{ artist }" -msgstr[4] "ألبوم %{ title } (%{ count } tracks) لِـ %{ artist }" -msgstr[5] "ألبوم %{ title } (%{ count } tracks) لِـ %{ artist }" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" @@ -285,7 +250,6 @@ msgstr "صفحة الألبوم" #: front/src/components/audio/Search.vue:19 #: src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "الألبومات" @@ -294,8 +258,8 @@ msgstr "الألبومات" msgid "Albums by this artist" msgstr "مِن ألبومات هذا الفنان" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "الكل" @@ -305,47 +269,23 @@ msgstr "حدث خطأ أثناء عملية حفظ التغييرات" #: front/src/components/auth/Login.vue:10 msgid "An unknown error happend, this can mean the server is down or cannot be reached" -msgstr "" - -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "الكل" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "واجهة برمجة التطبيقات API" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "وافق" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "ترخيص النفاذ ؟" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "مرخّص" +msgstr "طرأ هناك خطأ ما، ذلك قد يعني أن السيرفر غير متصل أو أنّ الإتصال به غير ممكن" #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "أمتأكد من أنك تريد الخروج ؟" -#: front/src/components/audio/SearchBar.vue:25 -#: src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 +#: src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" msgstr "الفنان" #: front/src/components/requests/Form.vue:5 +#: src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "إسم الفنان" @@ -354,26 +294,20 @@ msgstr "إسم الفنان" msgid "Artist page" msgstr "صفحة الفنان" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "فنان ، ألبوم ، مقطع موسيقي ..." - #: front/src/components/audio/Search.vue:10 #: src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 -#: src/components/library/Library.vue:7 +#: front/src/components/library/Library.vue:7 +#: src/components/library/Artists.vue:120 msgid "Artists" msgstr "الفنانون" #: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 -#: src/components/library/Radios.vue:44 +#: src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 -#: src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" msgstr "تصاعدي" @@ -381,10 +315,6 @@ msgstr "تصاعدي" msgid "Ask for a password reset" msgstr "أطلب إعادة تعيين كلمة المرور" -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "إستيراد تلقائي" - #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" msgstr "قوائم المقاطع الموسيقية المتوفرة" @@ -398,12 +328,9 @@ msgstr "الصورة الرمزية" msgid "Back to login" msgstr "العودة إلى صفحة تسجيل الدخول" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "" - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "معدل البت" @@ -411,7 +338,7 @@ msgstr "معدل البت" msgid "Browse" msgstr "تصفّح" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "تصفح المكتبة" @@ -419,18 +346,6 @@ msgstr "تصفح المكتبة" msgid "Browsing artists" msgstr "استعراض الفنانين" -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "تصفح المَقاطع الموسيقية المتحدة" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "إستكشاف المُشاركين" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "تصفّح المكتبات" - #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" msgstr "تصفّح قوائم المَقاطِع الموسيقية" @@ -447,22 +362,17 @@ msgstr "المحرّر" msgid "By %{ artist }" msgstr "حسب %{ artist }" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." -msgstr "" +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." +msgstr "إن قمت بإلغاء متابعة هذه المكتبة فسوف لن تتمكن مِن الوصول إلى محتواها." -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "" - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" msgstr "إلغاء" -#: front/src/components/library/radios/Builder.vue:59 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "" @@ -470,7 +380,7 @@ msgstr "" msgid "Cannot change your password" msgstr "لا يمكن تغيير كلمة المرور" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" msgstr "تغيير اللغة" @@ -497,13 +407,13 @@ msgstr "تمت مزامنة التعديلات مع السيرفر" #: front/src/components/auth/Settings.vue:70 msgid "Changing your password will also change your Subsonic API password if you have requested one." -msgstr "" +msgstr "سوف تتأثر كذلك الكلمة السرية لواجهة برمجة تطبيقات صاب سونيك إن قمت بتعديل كلمتك السرية." #: front/src/components/auth/Settings.vue:98 msgid "Changing your password will have the following consequences" msgstr "سوف ينجرّ ما يلي عند تعديل كلمتك السرية" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "اختر خادومك" @@ -520,22 +430,13 @@ msgstr "امسح" msgid "Clear playlist" msgstr "مسح قائمة المَقاطع الموسيقية" -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "مسح قائمة الانتظار الخاصة بك" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "واجهة إدخال سطر الأوامر CLI" - #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" msgstr "إضغط مرة واحدة و استمع لساعات مِن الموسيقى عبر الإذاعات المُدمَجة" -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" -msgstr "مُغلَقة" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" +msgstr "" #: front/src/components/manage/users/InvitationForm.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:42 @@ -546,16 +447,15 @@ msgstr "الرمز" #: front/src/components/audio/artist/Card.vue:33 #: front/src/components/discussion/Comment.vue:20 msgid "Collapse" -msgstr "" +msgstr "تصغير" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "تعليق" -#: front/src/components/library/radios/Builder.vue:58 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" -msgstr "" +msgstr "الإعداد" #: front/src/components/common/DangerousButton.vue:21 msgid "Confirm" @@ -570,11 +470,11 @@ msgstr "تأكيد بريدك الإلكتروني" msgid "Confirmation code" msgstr "رمز التأكيد" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" -msgstr "نسخ المَقاطِع مِن قائمة الإنتظار الحالية إلى قائمة التشغيل" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" +msgstr "نسخ" -#: front/src/components/Home.vue:88 +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" msgstr "أغلِفة الألبومات و كلمات الأغاني، هدفنا هو دمجها جميعا ;)" @@ -582,19 +482,26 @@ msgstr "أغلِفة الألبومات و كلمات الأغاني، هدفن msgid "Create a funkwhale account" msgstr "أنشئ حسابا على فانك وايل" +#: front/src/views/content/libraries/Home.vue:14 +msgid "Create a new library" +msgstr "أنشئ مكتبة جديدة" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "أنشئ قائمة مَقاطِع موسيقية جديدة" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "أنشئ حسابا" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "إنشاء استيراد" +#: front/src/views/content/libraries/Form.vue:26 +msgid "Create library" +msgstr "أنشئ مكتبة" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "أنشئ حسابي" @@ -607,9 +514,8 @@ msgstr "أنشئ قائمة مَقاطع موسيقية" msgid "Create your own radio" msgstr "أنشئ إذاعتك" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "تاريخ الإنشاء" @@ -617,17 +523,34 @@ msgstr "تاريخ الإنشاء" msgid "Current avatar" msgstr "الصورة الرمزية الحالية" +#: front/src/views/content/libraries/DetailArea.vue:4 +msgid "Current library" +msgstr "المكتبة الحالية" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "المَقطَع الحالي" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +msgid "Current usage" +msgstr "الاستعمال الحالي" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "التاريخ" + +#: front/src/views/content/libraries/Form.vue:29 +#: src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "حذف" +#: front/src/views/content/libraries/Form.vue:39 +msgid "Delete library" +msgstr "حذف المكتبة" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "حذف قائمة الأغاني" @@ -636,34 +559,32 @@ msgstr "حذف قائمة الأغاني" msgid "Delete radio" msgstr "حذف الإذاعة" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" -msgstr "رفض" - -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "هل تؤكد رفض الوصول ؟" +#: front/src/views/content/libraries/Form.vue:31 +msgid "Delete this library?" +msgstr "أتريد حذف هذه المكتبة؟" #: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 -#: src/components/library/Radios.vue:47 +#: src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 -#: src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "تنازليًا" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +msgid "Description" +msgstr "الوصف" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "التفاصيل" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." -msgstr "" +#: front/src/views/content/remote/Card.vue:50 +msgid "Details" +msgstr "التفاصيل" #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 @@ -682,7 +603,7 @@ msgstr "تعطيل النفاذ عبر واجهة برمجة التطبيقات msgid "Discover how to use Funkwhale from other apps" msgstr "إكتشف كيفية استخدام فانك وايل Funkwhale عبر التطبيقات الأخرى" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "إعرضها للعامة" @@ -712,41 +633,43 @@ msgstr[3] "" msgstr[4] "" msgstr[5] "" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "هل تريد استرجاع قائمة الإنتظار السابقة للأغاني ؟" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "الدليل" -#: front/src/components/audio/track/Table.vue:24 -#: src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "تنزيل" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "تنزيل" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" msgstr "" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 +#: src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "المدّة" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "سهل للإستخدام" +#: front/src/views/content/libraries/Detail.vue:9 +msgid "Edit" +msgstr "تعديل" + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "تعديل معلومات مثيل الخادوم" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "تعديل …" @@ -767,55 +690,16 @@ msgstr "عنوان البريد الإلكتروني مؤكَّد" msgid "End edition" msgstr "إنهاء التعديل" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "تأكّد مِن احتواء ملفات الموسيقى على بيانات وصفية صحيحة قبل إرسالها." - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "أدخِل إسم إذاعة …" - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "أدخِل إسم فنان …" - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "أدخِل إسم نطاق لمكتبة ما …" - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "أدخِل إسم قائمة مَقاطِعٍ موسيقية …" - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "أدخِل عنوان بريدك الإلكتروني" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "أدخِل رمز الدعوة" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "أدخِل طلب بحثِك …" - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "أدخِل إسم المستخدِم" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "أدخل إسم المستخدِم أو البريد الإلكتروني" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "خطأ" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" -msgstr "سِجِل الأخطاء" +#: front/src/views/content/remote/Card.vue:39 +msgid "Error during scan" +msgstr "خطأ أثناء الإستكشاف" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "حدث خطأ أثناء تطبيق الإجراء" @@ -835,29 +719,28 @@ msgstr "حدث خطأ أثناء تأكيد عنوان بريدك الإلكتر msgid "Error while creating invitation" msgstr "حدث خطأ أثناء إنشاء الدعوة" +#: front/src/views/content/remote/ScanForm.vue:3 +msgid "Error while fetching remote library" +msgstr "حدث خطأ أثناء عملية جلب المكتبة البُعدية" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "حدث خطأ أثناء حفظ الإعدادات" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "حدث خطأ أثناء عملية مسح المكتبة" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "فيه خطأ" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" -msgstr "الجميع" +#: front/src/views/content/libraries/Quota.vue:75 +msgid "Errored files" +msgstr "الملفات الخاطئة" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" -msgstr "كل مَن هم على مثيل الخادوم هذا" +#: front/src/views/content/remote/Card.vue:58 +msgid "Errored tracks:" +msgstr "المَقاطع الموسيقية الخاطئة:" -#: front/src/components/library/radios/Builder.vue:57 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "إستثني" @@ -866,6 +749,7 @@ msgid "Expand" msgstr "توسيع" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "تاريخ نهاية الصلاحية" @@ -877,97 +761,50 @@ msgstr "منتهية الصلاحيّة" msgid "Expired/used" msgstr "إنتهت صلاحيتها/ أو مستعمَلة" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "مصدر خارجي. الخدمات المدعومة" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "المفضلة" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "إتّحد مع مثيل خادوم جديد" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "المَقاطع الموسيقية المتحدة" - -#: front/src/components/Sidebar.vue:87 -#: src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 -#: src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 -#: src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "الفديرالية" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "النسخ المتماثل للملفات" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "إسم الملفّ" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "إرسال ملفّ" - #: front/src/views/admin/library/Base.vue:5 #: src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "الملفّات" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "تصفية نوع الألبومات" - -#: front/src/components/library/radios/Builder.vue:56 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "إسم عامل التصفية" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "إنهاء الإستيراد" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "إكتمل" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "أولًا قم باختيار أسلوب استيراد و جلب الموسيقى" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "إتبع" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" -msgstr "طلبات متابعة مُعلّقة" +#: front/src/views/content/remote/Card.vue:88 +msgid "Follow pending approval" +msgstr "طلبات متابعة مُعلّقة تنتظر اجراء" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" -msgstr "حالة الإشتراك" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" +msgstr "متابعة المكتبات عن بُعد" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "المتابِعون" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "المتابِعون فقط" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "يُتابِع" -#: front/src/components/activity/Like.vue:12 -#: src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 +#: src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "مِن %{ album } لِـ %{ artist }" @@ -975,7 +812,7 @@ msgstr "مِن %{ album } لِـ %{ artist }" msgid "From album %{ album } by %{ artist }" msgstr "مِن ألبوم %{ album } لِـ %{ artist }" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" msgstr "فانك وايل Funkwhale مشروع حُر و مفتوح المصدر يديره متطوعون. يمكنك مساعدتنا على تحسين المنصة عن طريق الإبلاغ عن أخطاء، و اقتراح ميزات بما في ذلك مشاركة المشروع مع أصدقائك !" @@ -983,7 +820,7 @@ msgstr "فانك وايل Funkwhale مشروع حُر و مفتوح المصدر msgid "Funkwhale is compatible with other music players that support the Subsonic API." msgstr "فانك وايل Funkwhale متوافق مع برمجيات تشغيل الموسيقى التي تدعم واجهة برمجية تطبيقات صاب سونيك." -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "فانك وايل Funkwhale سهلٌ جدًا للإستخدام." @@ -991,7 +828,7 @@ msgstr "فانك وايل Funkwhale سهلٌ جدًا للإستخدام." msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." msgstr "طُوِّر فانك وايل Funkwhale لتسهيل الإستماع إلى الموسيقى التي تحبونها و لاكتشاف فنّانين جُدد." -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." msgstr "فانك وايل Funkwhale مجاني و يُعيد التحكّم في موسيقاكم بين أيديكم." @@ -1007,17 +844,15 @@ msgstr "تحصّل على دعوة جديدة" msgid "Get me to the library" msgstr "أنقلني إلى المكتبة" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " +#: front/src/components/Home.vue:76 +msgid "Get quality metadata about your music thanks to MusicBrainz" msgstr "" -"أحصلوا على بيانات وصفية ذات جودة بفضل\n" -" \n" -" MusicBrainz\n" -" " +"احصلوا على بيانات وصفية ذات جودة عن موسيقاكم بفضل MusicBrainz" + +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +msgid "Get started" +msgstr "إبدأ هنا" #: front/src/components/common/ActionTable.vue:21 #: front/src/components/common/ActionTable.vue:27 @@ -1028,18 +863,10 @@ msgstr "هيا" msgid "Go to home page" msgstr "إنتقل إلى الصفحة الرئيسية" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "جلب البيانات الوصفية ذات الصّلة" - -#: front/src/App.vue:74 +#: front/src/App.vue:73 msgid "Help us translate Funkwhale" msgstr "ساعدنا على ترجمة فانك وايل" -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "الرئيسية" - #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" msgstr "ساعات مِن الموسيقى" @@ -1048,132 +875,35 @@ msgstr "ساعات مِن الموسيقى" msgid "However, accessing Funkwhale from those clients require a separate password you can set below." msgstr "" -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "المعرّف" - #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." msgstr "" -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "استيراد" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "إنطلَقت عملية الإستيراد #%{ id }" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "إستيراد %{ count } مَقطَع" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "إستيراد %{ count } مَقاطِع" -msgstr[4] "إستيراد %{ count } مَقاطِع" -msgstr[5] "إستيراد %{ count } مَقاطِع" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "إستيراد بالجملة" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "حُزم الإستيراد بالجملة" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "" - #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "تاريخ الإستيراد" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "إستيراد تفاصيل الصفحة" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "استيراد الموسيقى" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "استيراد الموسيقى" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" msgstr "استيراد الموسيقى من منصات مختلفة، مثل يوتيوب أو ساوند كلاود" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" -msgstr "الواردات المعلّقة" - -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "طلبات الإستيراد" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" +#: front/src/components/library/FileUpload.vue:51 +msgid "Import reference" msgstr "مصدر الإستيراد" -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "حالة الإستيراد" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "أجلب هذا الألبوم" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "استورد هذا المَقطع" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "تم استيراده" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "تم استيراد عنوان الرابط" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "في المفضلة" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "على المكتبة" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "غير ناشط" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "أدخِل يدويًا مُعرّف MusicBrainz ID :" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "أدخِل عنوان البريد الإلكتروني المتّصل بحسابك" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" @@ -1184,22 +914,10 @@ msgstr[3] "" msgstr[4] "" msgstr[5] "" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "معلومات عن مثيل الخادوم" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "إذاعات مثيل الخادوم" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "إعدادات مثيل الخادوم" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "الخيط الزمني لمثيل الخادوم" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1215,49 +933,41 @@ msgstr "رمز الدعوة (اختياري)" msgid "Invitations" msgstr "الدعوات" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "متعقّب المشاكل" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "مُعرّف الإجراء" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" -msgstr "الإجراءات" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" +msgstr "يبدو أنه ليس لديك أية مكتبة بعد، حان الأوان لإنشاء واحدة!" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" msgstr "حافظوا على أثر موسيقاكم و أغانيكم المفضّلة" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." -msgstr "" +#: front/src/views/content/remote/Home.vue:14 +msgid "Known libraries" +msgstr "المكتبات المعروفة" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "آخر نشاط" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "آخِر عملية جلب" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "آخر تعديل" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +msgid "Last update:" +msgstr "آخِر تحديث:" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "إبدأ" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "تاريخ الإطلاق" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" msgstr "إبدأ المسح" @@ -1265,25 +975,21 @@ msgstr "إبدأ المسح" msgid "Learn more about this instance" msgstr "إعرف المزيد عن مثيل الخادوم هذا" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "أتركه فارغًا للحصول على رمز عشوائي" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "دع هذا الحقل فارغا إن كنت ترغب في كافة الألبومات." -#: front/src/views/federation/Base.vue:5 -#: src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "المكتبات" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "" + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "المكتبة" @@ -1291,156 +997,111 @@ msgstr "المكتبة" msgid "Library files" msgstr "ملفّات المكتبة" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "إسم المكتبة" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "حجم المكتبة" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "library@demo.funkwhale.audio" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "الروابط" +#: front/src/views/content/libraries/Detail.vue:21 +msgid "Loading followers..." +msgstr "تحميل المتابعين…" + +#: front/src/views/content/libraries/Home.vue:3 +msgid "Loading Libraries..." +msgstr "جارٍ تحميل المكتبات …" + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +msgid "Loading library data..." +msgstr "جارٍ تحميل بيانات المكتبات…" + +#: front/src/views/Notifications.vue:4 +msgid "Loading notifications..." +msgstr "عملية تحميل الإشعارات جارية…" + +#: front/src/views/content/remote/Home.vue:3 +msgid "Loading remote libraries..." +msgstr "عملية تحميل المكتبات البُعدية جارية…" + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "عملية تحميل الخيط الزمني جارية …" +#: front/src/views/content/libraries/Quota.vue:4 +msgid "Loading usage data..." +msgstr "جارٍ تحميل بيانات الإستخدام…" + #: front/src/components/favorites/List.vue:5 msgid "Loading your favorites..." msgstr "جارٍ تحميل مفضلاتك …" -#: front/src/components/auth/Login.vue:78 -msgid "Log In" -msgstr "الدخول" - #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "الدخول إلى حسابك على فانك وايل Funkwhale" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "الخروج" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "مُتّصل كـ %{ username }" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "الدخول" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "خروج" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "" - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "" - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "" - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "الكلمات" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "إدارة المكتبة" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "إدارة قوائم المَقاطِع الموسيقية" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "إدارة المستخدِمين" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "إدارة القوائم الخاصة الموسيقى" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" -msgstr "" +#: front/src/views/Notifications.vue:17 +msgid "Mark all as read" +msgstr "تحديد الكل كمقروء" -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" -msgstr "" +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" +msgstr "MB" -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "البيانات الوصفية" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." -msgstr "" -"البيانات الوصفية هي الم البيانات المتعلقة بالموسيقى التي تريد استيرادها. و " -"هي تحتوي على معلومات عن الفنانين و الألبومات و المَقاطِع الموسيقية. و بغرض " -"إنشاء مكتبة بها جودة، يُستحسن جلب البيانات مِن \n" -" \n" -" MusicBrainz\n" -" \n" -" مشروع بمثابة ويكيبيديا للموسيقى." - -#: front/src/components/Sidebar.vue:48 -#: src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "الموسيقى" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "طلب موسيقى" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "كتم" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "حسابي" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" -msgstr "قائمتي الرائعة للمَقاطِع الموسيقية" +#: front/src/views/content/libraries/Home.vue:6 +msgid "My libraries" +msgstr "مكتباتي" -#: front/src/components/library/radios/Builder.vue:227 -msgid "My awesome radio" -msgstr "إذاعتي الرائعة" - -#: front/src/components/library/Track.vue:64 -#: src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 +#: src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 +#: src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "غير متوفر" #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "الإسم" @@ -1449,48 +1110,39 @@ msgstr "الإسم" msgid "New password" msgstr "الكلمة السرية الجديدة" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "سوف يتم إضافة المَقاطِع الجديدة هنا آليًا." -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "الخطوة التالية" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "المَقطَع التالي" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "لا" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" msgstr "لا تحتاج إلى تنصيب إضافاتٍ أو مُلحَقاتٍ : كل ما تحتاج إليه هي مكتبة موسيقية على الويب" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "لا تتوفّر هناك كلمات لهذا المَقطَع." -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" -msgstr "لا أحد غيري" +#: front/src/components/federation/LibraryWidget.vue:6 +msgid "No matching library." +msgstr "ليس هناك أية مكتبة مطابِقة." -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "غير مشترِك" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" -msgstr "غير مُستورَد" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" +msgstr "لا أحد يتبع هذه المكتبة" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "غير مستعمَل" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +msgid "Notifications" +msgstr "الإشعارات" + +#: front/src/App.vue:36 msgid "Official website" msgstr "موقع الويب الرسمي" @@ -1498,47 +1150,32 @@ msgstr "موقع الويب الرسمي" msgid "Old password" msgstr "الكلمة السرية الجديدة" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "" -"حينما تتم عملية إرسال كافة ملفاتك، إضغط على الزر التالي للتحقق مِن حالة " -"الإستيراد." - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 msgid "Open" msgstr "مفتوح" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "الخيارات" -#: front/src/components/library/import/Main.vue:93 -msgid "Or" -msgstr "أو" - #: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 -#: src/components/library/Radios.vue:33 +#: src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 -#: src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" msgstr "الترتيب" #: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 -#: src/components/library/Radios.vue:41 +#: src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 -#: src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "اتجاه الترتيب" @@ -1546,10 +1183,6 @@ msgstr "اتجاه الترتيب" msgid "Owner" msgstr "المالك" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "الصفحة غير موجودة" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "الصفحة غير موجودة !" @@ -1558,42 +1191,25 @@ msgstr "الصفحة غير موجودة !" msgid "Password" msgstr "كلمة السر" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "تم تحديث كلمة السر" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "تم تحديث كلمة السر بنجاح" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "ألبِث المَقطَع" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "معلّق" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" msgstr "في انتظار التسريح" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" -msgstr "طلبات المتابَعة المعلَّقة" +#: front/src/views/content/libraries/Quota.vue:22 +msgid "Pending files" +msgstr "الملفات المعلّقة" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "طلبات الإستيراد المعلّقة" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "الطلبات المعلَّقة" @@ -1617,10 +1233,6 @@ msgstr "تشغيل الكل" msgid "Play all albums" msgstr "إعزف كافة الألبومات" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "إعزف في الحين" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "إعزف التالي" @@ -1629,14 +1241,6 @@ msgstr "إعزف التالي" msgid "Play now" msgstr "إعزف الآن" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "إعزف المَقطَع" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "قائمة المَقاطِع" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1667,9 +1271,9 @@ msgstr "تم تحديث قائمة تشغيل الموسيقى" msgid "Playlist visibility" msgstr "مدى رؤية القائمة" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" msgstr "قوائم المَقاطِع" @@ -1687,47 +1291,46 @@ msgstr "الرجاء التأكّد مِن صحة اسم المستخدِم و #: front/src/components/auth/Settings.vue:46 msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." -msgstr "" -"نسق PNG أو GIF أو JPG. الحجم الأقصى 2 ميغابيت. سيتم تغيير حجمها إلى 400×400 " -"بكسل." +msgstr "نسق PNG أو GIF أو JPG. الحجم الأقصى 2 ميغابيت. سيتم تغيير حجمها إلى 400×400 بكسل." -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "الخطوة السابقة" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" -msgstr "المَقطَع السابق" +#: front/src/components/library/FileUpload.vue:58 +msgid "Proceed" +msgstr "واصل" #: front/src/views/auth/EmailConfirm.vue:26 #: front/src/views/auth/PasswordResetConfirm.vue:31 msgid "Proceed to login" msgstr "المواصلة إلى صفحة تسجيل الدخول" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" -msgstr "تاريخ النشر" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" +msgstr "" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" -msgstr "نموذج البحث" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "هل تريد إزالة الملفات المعلّقة؟" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" +msgstr "هل تريد إزالة الملفات المتخطاة؟" #: front/src/components/Sidebar.vue:20 msgid "Queue" msgstr "قائمة الإنتظار" -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "تم خلط قائمة الإنتظار !" - -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "الإذاعة" - -#: front/src/components/library/radios/Builder.vue:226 -msgid "Radio Builder" -msgstr "مُنشِئ الإذاعات و الراديو" - #: front/src/components/library/radios/Builder.vue:15 msgid "Radio created" msgstr "تم إنشاء الإذاعة" @@ -1741,7 +1344,7 @@ msgid "Radio updated" msgstr "تم تحديث الإذاعة" #: front/src/components/library/Library.vue:10 -#: src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 msgid "Radios" msgstr "الإذاعات" @@ -1761,19 +1364,11 @@ msgstr "تمت إضافتها إلى المفضلة حديثا" msgid "Recently listened" msgstr "مَقاطِع أستُمِع إليها مؤخرا" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "مُعرّف التسجيل الصوتي على ميوزيك براينز MusicBrainz" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" msgstr "إنعاش" -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "رفض" - #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" msgstr "مُسجّل منذ %{ date }" @@ -1786,6 +1381,22 @@ msgstr "إنّ التسجيلات مُغلَقة حاليًا على هذا ال msgid "regular user" msgstr "مستخدِم عادي" +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "رفض" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "تم رفضه" + +#: front/src/views/content/remote/Home.vue:6 +msgid "Remote libraries" +msgstr "المكتبات البُعدية" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." +msgstr "" + #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" msgstr "حذف" @@ -1794,10 +1405,6 @@ msgstr "حذف" msgid "Remove avatar" msgstr "حذف الصورة الرمزية" -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "حذف مِن المفضلة" - #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" @@ -1811,109 +1418,60 @@ msgstr "متأكد مِن أنك تريد إعادة طلب كلمة سرية ج msgid "Request a password" msgstr "طلب كلمة سرية" -#: front/src/App.vue:35 -msgid "Request music" -msgstr "طلب موسيقى" - -#: front/src/views/library/MusicRequest.vue:4 -#: src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "طلب بعض مِن الموسيقى" - #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" msgstr "تم إرسال الطلب !" -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "" - #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" msgstr "إعادة تعيين كلمتك السرية" -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "النتيجة %{ current }/%{ total }" - #: front/src/components/favorites/List.vue:38 #: src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 -#: src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" msgstr "عدد نتائج البحث في كل صفحة" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:29 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" msgstr "احفظ" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" -msgstr "إنطَلَقَ المسح !" +#: front/src/views/content/remote/Card.vue:31 +msgid "Scan pending" +msgstr "المسح المعلق" + +#: front/src/views/content/remote/Card.vue:43 +msgid "Scanned successfully" +msgstr "تمت عملية المسح بنجاح" + +#: front/src/views/content/remote/Card.vue:47 +msgid "Scanned with errors" +msgstr "تمت عملية المسح بأخطاء" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" +msgstr "جارٍ المسح (%{ progress }%)" -#: front/src/components/federation/LibraryTrackTable.vue:5 #: front/src/components/library/Artists.vue:10 #: src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:5 #: src/views/playlists/List.vue:13 msgid "Search" msgstr "البحث" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "البحث باستخدام إسم فنان أو مستخدِم أو تعليق …" - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "البحث حسب المصدر …" - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "البحث حسب المستخدِم أو المصدر …" - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "البحث حسب العنوان أو إسم فنان أو نطاق …" - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "البحث حسب إسم المستخدِم أو النطاق …" - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "البحث باسم مستخدِم أو عنوان بريد إلكتروني أو رمز …" - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "البحث باسم مستخدِم أو عنوان بريد إلكتروني أو إسم …" - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." -msgstr "البحث عن فنانين أو ألبومات أو مَقاطِع موسيقية …" +#: front/src/views/content/remote/ScanForm.vue:9 +msgid "Search a remote library" +msgstr "البحث عن مكتبة بُعدية" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" msgstr "البحث عن بعض مِن الموسيقى" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" msgstr "البحث في lyrics.wikia.com" @@ -1923,19 +1481,15 @@ msgstr "البحث في lyrics.wikia.com" msgid "Search on Wikipedia" msgstr "البحث في ويكيبيديا" -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "البحث" - #: front/src/views/admin/Settings.vue:15 msgid "Sections" msgstr "الأقسام" -#: front/src/components/library/radios/Builder.vue:41 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" msgstr "إختيار عامل تصفية" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" msgstr[0] "" @@ -1945,25 +1499,13 @@ msgstr[3] "" msgstr[4] "" msgstr[5] "" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "إختيار الملفات الجاهزة للإرسال …" - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" msgstr "تحديد الصفحة الحالية فقط" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "إختيار المصادر أو الملفات الجاهزة للإستيراد" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "إرسال طلب متابَعة" - -#: front/src/components/Sidebar.vue:97 -#: src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" msgstr "الإعدادات" @@ -1975,14 +1517,30 @@ msgstr "تم تحديث الإعدادات" msgid "Settings updated successfully." msgstr "تم تحديث الإعدادات بنجاح." -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "الإعدادات …" - #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" msgstr "رابط المشاركة" +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." +msgstr "قم بمشاركة هذا الرابط مع مستخدمين آخرين ليتمكنوا مِن طلب الوصول إلى مكتبتك." + +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +msgid "Sharing link" +msgstr "رابط المشاركة" + +#: front/src/components/audio/album/Card.vue:40 +#, fuzzy +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "%{ count } مقاطع" +msgstr[1] "%{ count } مَقطَع" +msgstr[2] "%{ count } مَقاطِع" +msgstr[3] "%{ count } مَقطَع" +msgstr[4] "%{ count } مَقاطِع" +msgstr[5] "%{ count } مَقاطِع" + #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" @@ -1993,59 +1551,39 @@ msgstr[3] "" msgstr[4] "" msgstr[5] "" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "" -msgstr[4] "" -msgstr[5] "" +#: front/src/views/Notifications.vue:10 +msgid "Show read notifications" +msgstr "عرض الإشعارات المقروءة" -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" -msgstr "إظهار/إخفاء الكلمة السرية" - -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" msgstr "عرض النتائج %{ start }-%{ end } مِن %{ total }" -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "خلط قائمة الإنتظار" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "التسجيل" - #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" msgstr "التسجيل" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "" - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 +#: src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" msgstr "الحجم" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 msgid "Skipped" msgstr "تمّ تجاهله" +#: front/src/views/content/libraries/Quota.vue:49 +msgid "Skipped files" +msgstr "الملفات التي تمّ تجاهلها" + #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "هناك شيء ما ناقص في هذه المكتبة ؟ أبلغنا عما تريد الإستماع إليه !" @@ -2058,21 +1596,15 @@ msgstr "المعذرة، لم نتمكّن مِن العثور على أي أل msgid "Sorry, we did not found any artist matching your query" msgstr "المعذرة، لم نتمكّن مِن العثور على أي فنان يناسب طلب بحثك" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "المصدر" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "شفرة المصدر" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "الشفرة المصدرية (%{version})" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" msgstr "عضو في الفريق" @@ -2081,26 +1613,11 @@ msgstr "عضو في الفريق" msgid "Start" msgstr "إبدأ" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "إبدأ الإرسال" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "الإحصائيات" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "الحالة" @@ -2108,15 +1625,11 @@ msgstr "الحالة" msgid "Stop" msgstr "إيقاف" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" msgstr "إيقاف الإذاعة" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "إيقاف الإرسال" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" msgstr "إرسال" @@ -2124,80 +1637,81 @@ msgstr "إرسال" msgid "Submit another request" msgstr "إرسال طلب جديد آخَر" -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "إقترَحه" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "صاب سونيك" - #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" msgstr "الكلمة السرية لواجهة برمجة التطبيقات صاب سونيك Subsonic" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" -msgstr "نجح" - -#: front/src/App.vue:11 +#: front/src/App.vue:13 msgid "Suggested choices" msgstr "الخيارات المتاحة" +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" +msgstr "الملخص" + #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "مزامنة التغييرات مع الخادم …" +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "تم نسخ النص إلى الحافظة!" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." msgstr "حسنًا الأمر سهل : أحببنا غروف شارْك و أردنا تصميم مشروع أحسَن منه بكثير." -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "The Beatles, Mickael Jackson…" - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." msgstr "تم تصميم شعار فانك وايل funkwhale بفضل و كَرَم Francis Gading." -#: front/src/components/Home.vue:124 +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." +msgstr "سوف يتم حذف المكتبة و كل ما تحتويه مِن مقاطِع. لا يمكن التراجع عن هذا الإجراء." + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "الملفات الموسيقية التي هي في صدد الإرسال موسومة بطريقة صحيحة:" + +#: front/src/components/Home.vue:121 msgid "The plaform is free and open-source, you can install it and modify it without worries" msgstr "المنصّة مجانية و مفتوحة المصدر، بإمكانكم تنصيبها و تعديلها كما يحلو لكم دون قيود" #: front/src/components/auth/SubsonicTokenForm.vue:4 msgid "The Subsonic API is not available on this Funkwhale instance." +msgstr "واجهة برمجة تطبيقات صاب سونيك غير متوفرة غلى مثيل خادوم فانك وايل الحالي." + +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" msgstr "" -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" -msgstr "The White Album, Thriller…" +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" +msgstr "هذا الألبوم متوفر على المكتبات التالية:" -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." -msgstr "" +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" +msgstr "هذا الفنان متوفر على المكتبات التالية:" -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" -msgstr "" +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." +msgstr "مثيل الخادوم هذا يُتيح حجم تخرين يُقدَّر بـ %{quota} لكل مستخدِم." #: front/src/components/auth/Profile.vue:16 msgid "This is you!" msgstr "هذا أنت !" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." msgstr "" -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" -msgstr "لم يتم استيراد هذا المَقطَع لذا لا يمكن الإستماع إليه" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "" + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" +msgstr "هذا المقطع متوفر كذلك على المكتبات التالية:" #: front/src/views/playlists/Detail.vue:37 msgid "This will completely delete this playlist and cannot be undone." @@ -2211,9 +1725,168 @@ msgstr "سوف يؤدي ذلك إلى الحذف الكُلّي لهذه الإ msgid "This will completely disable access to the Subsonic API using from account." msgstr "" -#: front/src/App.vue:137 +#: front/src/App.vue:162 src/components/About.vue:55 +#: src/components/Home.vue:154 front/src/components/PageNotFound.vue:33 +#: src/components/Sidebar.vue:203 front/src/components/Sidebar.vue:204 +#: src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 +#: src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 +#: src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 +#: src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 +#: src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 +#: src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 +#: src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 +#: src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 +#: src/components/audio/SearchBar.vue:27 front/src/components/auth/Login.vue:77 +#: src/components/auth/Login.vue:78 front/src/components/auth/Logout.vue:20 +#: src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 +#: src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 +#: src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 +#: src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 +#: src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 +#: src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 +#: src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 +#: src/views/auth/EmailConfirm.vue:51 front/src/views/auth/PasswordReset.vue:52 +#: src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +#: src/views/content/Base.vue:19 front/src/views/content/Home.vue:35 +#: src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 +#: src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 +#: src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 +#: src/views/instance/Timeline.vue:57 front/src/views/playlists/Detail.vue:90 +#: src/views/playlists/List.vue:104 front/src/views/playlists/List.vue:105 +#: src/views/radios/Detail.vue:80 +#, fuzzy msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "ذلك سوف يؤدي إلى حذف بياناتك المحلية نهائيا و إخراجك. أمتأكد أنك ترغب في المواصلة ؟" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "ذلك سوف يؤدي إلى حذف بياناتك المحلية نهائيا و إخراجك. أمتأكد أنك ترغب في المواصلة ؟" +msgstr[1] "ذلك سوف يؤدي إلى حذف بياناتك المحلية نهائيا و إخراجك. أمتأكد أنك ترغب في المواصلة ؟" +msgstr[2] "ذلك سوف يؤدي إلى حذف بياناتك المحلية نهائيا و إخراجك. أمتأكد أنك ترغب في المواصلة ؟" +msgstr[3] "ذلك سوف يؤدي إلى حذف بياناتك المحلية نهائيا و إخراجك. أمتأكد أنك ترغب في المواصلة ؟" +msgstr[4] "ذلك سوف يؤدي إلى حذف بياناتك المحلية نهائيا و إخراجك. أمتأكد أنك ترغب في المواصلة ؟" +msgstr[5] "ذلك سوف يؤدي إلى حذف بياناتك المحلية نهائيا و إخراجك. أمتأكد أنك ترغب في المواصلة ؟" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." @@ -2223,19 +1896,25 @@ msgstr "" msgid "This will remove all tracks from this playlist and cannot be undone." msgstr "" +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + #: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 #: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 msgid "Title" msgstr "العنوان" -#: front/src/components/audio/SearchBar.vue:27 -#: src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" -msgstr "المَقطَع" - #: front/src/components/library/Track.vue:53 msgid "Track information" msgstr "معلومات عن المَقطَع" @@ -2250,15 +1929,11 @@ msgstr "مَقاطِع" #: front/src/components/library/Album.vue:43 #: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 -#: front/src/views/radios/Detail.vue:34 +#: src/views/content/Base.vue:8 front/src/views/content/libraries/Detail.vue:8 +#: src/views/playlists/Detail.vue:50 front/src/views/radios/Detail.vue:34 msgid "Tracks" msgstr "المَقاطِع" -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "المَقاطِع المتوفّرة في هذه المكتبة" - #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" msgstr "مَقاطِع لهذا الفنان" @@ -2271,34 +1946,36 @@ msgstr "مَقاطِع تم الإعجاب بها" msgid "tracks listened" msgstr "مَقاطِع أستُمِع إليها" -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "" - +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" msgstr "النوع" +#: front/src/views/content/remote/Card.vue:100 +#: src/views/content/remote/Card.vue:105 +msgid "Unfollow" +msgstr "إلغاء المتابعة" + +#: front/src/views/content/remote/Card.vue:101 +msgid "Unfollow this library?" +msgstr "أتريد إلغاء متابعة هذه المكتبة؟" + #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." msgstr "لسوء الحظ، لم يأخذ أصحاب مثيل الخادوم هذا الوقت الكافي لاستكمال هذه الصفحة." -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "مجهول" - #: front/src/components/Home.vue:37 msgid "Unlimited music" msgstr "موسيقى بلا حدود" -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "إلغاء الكتم" - #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" msgstr "تحديث الصورة الرمزية" +#: front/src/views/content/libraries/Form.vue:25 +msgid "Update library" +msgstr "تحديث المكتبة" + #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" msgstr "تحديث قائمة المَقاطِع الموسيقية" @@ -2311,8 +1988,10 @@ msgstr "تحديث الإعدادات" msgid "Update your password" msgstr "قم بتحديث كلمتك السرية" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" msgstr "أرسل" @@ -2320,39 +1999,51 @@ msgstr "أرسل" msgid "Upload a new avatar" msgstr "إرسال صورة رمزية جديدة" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" -msgstr "ملفات مُرسَلة أو مصدر خارجي" +#: front/src/views/content/Home.vue:6 +msgid "Upload audio content" +msgstr "إرسال محتوى صوتي" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/views/content/libraries/FilesTable.vue:54 +msgid "Upload date" +msgstr "تاريخ التحميل" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:31 +msgid "Upload new tracks" +msgstr "إرسال مقاطع موسيقية جديدة" + +#: front/src/views/admin/users/UsersDetail.vue:82 +msgid "Upload quota" +msgstr "حصة التحميل" + +#: front/src/components/library/FileUpload.vue:99 +msgid "Uploaded" +msgstr "تم تحميلها" + +#: front/src/components/library/FileUpload.vue:5 +msgid "Uploading" +msgstr "عملية الإرسال جارية" + +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "عملية الإرسال جارية …" -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" msgstr "إستخدم مثيل خادوم آخَر" -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "استخدم علبة التعليق هذه، إن دعت الحاجة، لإضافة تفاصيل طلبك" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "" - #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." msgstr "استخدم هذه الإستمارة لطلب إعادة ضبط كلمة المرور. سنرسل بريدا الكترونيا إلى العنوان المعين مرفوقا بتعليمات لإعادة ضبط كلمتك السرية." -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "" - #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" msgstr "مُستخدَم" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" msgstr "المستخدِم" @@ -2360,12 +2051,19 @@ msgstr "المستخدِم" msgid "User activity" msgstr "نشاط المستخدِم" +#: front/src/components/library/Album.vue:49 +#: src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +msgid "User libraries" +msgstr "مكتبات المستخدِم" + #: front/src/components/library/Radios.vue:20 msgid "User radios" msgstr "إذاعات المستخدِمين" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" msgstr "إسم المستخدِم" @@ -2377,21 +2075,30 @@ msgstr "إسم المستخدِم أو عنوان البريد الإلكترو msgid "users" msgstr "مستخدِم" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 +#: src/views/admin/users/UsersList.vue:21 front/src/views/admin/Settings.vue:81 msgid "Users" msgstr "المستخدِمون" +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +msgid "View files" +msgstr "عرض الملفات" + #: front/src/components/library/Album.vue:37 #: src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" msgstr "إطّلع عليه على ميوزيك براينز" +#: front/src/views/content/libraries/Form.vue:18 +msgid "Visibility" +msgstr "المشاهدة" + #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" msgstr "لا يمكننا إضافة المَقطَع إلى قائمة التشغيل" @@ -2416,13 +2123,21 @@ msgstr "تعذّر علينا حفظ صورتك الرمزية" msgid "We cannot save your settings" msgstr "تعذّر علينا حفظ إعداداتك" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" msgstr "لا نتعقّبك و لا نزعجك بالإعلانات" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." -msgstr "ننصحكم باستخدام برنامج Picard لهذا الغرض." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "ليس لدينا أي اشعار لعرضه هنا!" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." +msgstr "نوفر لكم عدة أساليب لجلب محتويات جديدة و عرضها هنا." + +#: front/src/components/library/FileUpload.vue:40 +msgid "we recommend using Picard for that purpose" +msgstr "ننصحكم باستخدام برنامج Picard لهذا الغرض" #: front/src/components/Home.vue:7 msgid "We think listening to music should be simple." @@ -2436,27 +2151,15 @@ msgstr "المعذرة، إنّ الصفحة التي قمت بطلبها غير msgid "We've received your request, you'll get some groove soon ;)" msgstr "" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "مرحبًا" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" msgstr "أهلا وسهلا بك على فانك وايل Funkwhale" -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "ما المقصود بالبيانات الوصفية ؟" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "" - #: front/src/components/Home.vue:24 msgid "Why funkwhale?" msgstr "لماذا فانك وايل Funkwhale ؟" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" msgstr "نعم" @@ -2464,58 +2167,62 @@ msgstr "نعم" msgid "Yes, log me out!" msgstr "نعم، أؤكد الخروج !" +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" +msgstr "إنك بصدد إرسال موسيقى إلى مكتبتك الصوتية. قبل المواصلة، ندعوك إلى التحقق من أنّ:" + #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" msgstr "أنت متّصل حاليا بصفة %{ username }" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." -msgstr "يمكنك طبعًا تخطي هذه الخطوة و إدخال البيانات الوصفية يدويًا." +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." +msgstr "" -#: front/src/components/Home.vue:136 +#: front/src/components/Home.vue:133 msgid "You can invite friends and family to your instance so they can enjoy your music" msgstr "يمكِنك دعوة أصدقائك و عائلتك للإنظمام إلى مثيل خادومك للإستمتاع بموسيقاك" #: front/src/components/library/radios/Builder.vue:7 msgid "You can use this interface to build your own custom radio, which will play tracks according to your criteria." -msgstr "" +msgstr "يمكنك إنشاء قناتك الإذاعية الخاصة بك عبر هذه الواجهة و تشغيل مقاطعك كيفما شئت." #: front/src/components/auth/SubsonicTokenForm.vue:8 msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." msgstr "" -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" msgstr "إنك تستمع إلى إذاعة" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" msgstr "يلزمك اختيار مثيل خادوم قصد المواصلة" +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." +msgstr "" + #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" msgstr "" #: front/src/components/auth/Settings.vue:71 msgid "You will have to update your password on your clients that use this password." -msgstr "" - -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "أنت بصدد استيراد :" +msgstr "يتوجب عليك تحديث كلمتك السرية على العملاء الآخرين اللذين يشتغلون بها." #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." msgstr "تمّ تأكيد عنوان بريدك الإلكتروني، بإمكانك الآن استعمال الخدمة مِن دون قيود." -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" -msgstr "مفضّلاتك" - -#: front/src/components/Home.vue:117 +#: front/src/components/Home.vue:114 msgid "Your music, your way" msgstr "موسيقاك، كما يحلو لك" +#: front/src/views/Notifications.vue:7 +msgid "Your notifications" +msgstr "إشعاراتك" + #: front/src/views/auth/PasswordResetConfirm.vue:29 msgid "Your password has been updated successfully." msgstr "تم تحديث كلمتك السرية بنجاح." @@ -2524,7 +2231,181 @@ msgstr "تم تحديث كلمتك السرية بنجاح." msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" msgstr "" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +msgid "Activity visibility" +msgstr "عرض النشاط" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 +#: src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "لا أحد غيري" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 +#: src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "كل مَن هم على مثيل الخادوم هذا" + +#: front/src/components/mixins/Translations.vue:18 +msgid "Accessed date" +msgstr "تاريخ النفاذ" + +#: front/src/components/mixins/Translations.vue:19 +msgid "Modification date" +msgstr "تاريخ التعديل" + +#: front/src/components/mixins/Translations.vue:20 +msgid "Imported date" +msgstr "تاريخ الإستيراد" + +#: front/src/components/mixins/Translations.vue:22 +msgid "Track name" +msgstr "اسم المَقطَع" + +#: front/src/components/mixins/Translations.vue:23 +msgid "Album name" +msgstr "عنوان الألبوم" + +#: front/src/components/mixins/Translations.vue:30 +msgid "Sign-up date" +msgstr "تاريخ التسجيل" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "نسخ المَقاطِع مِن قائمة الإنتظار الحالية إلى قائمة التشغيل" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "أضفه إلى قائمة التشغيل هذه" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "قائمتي الرائعة للمَقاطِع الموسيقية" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "الجميع" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "التسجيل" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "أدخِل رمز الدعوة" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "أدخِل إسم المستخدِم" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "أدخِل عنوان بريدك الإلكتروني" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "تم تحديث كلمة السر" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "عُطّل النفاذ" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "أدخل إسم المستخدِم أو البريد الإلكتروني" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "الدخول" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "الملف الشخصي لِـ %{ username }" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "الخروج" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "إعدادات الحساب" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "حذف مِن المفضلة" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "مفضّلاتك" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "أدخِل إسم إذاعة …" + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "مُنشِئ الإذاعات و الراديو" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "إذاعتي الرائعة" + +#: front/src/components/library/radios/Builder.vue:236 +msgid "My awesome description" +msgstr "وصفي الرائع" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "تم رفض الإرسال، تحقق أن حجم الملف ليس ضخما و أنّ مساحة التخرين المسموح بها كافية" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "لا يمكن تحميل هذا الملف، تحقق أنّ حجم الملف ليس ضخما" + +#: front/src/components/library/FileUpload.vue:240 +msgid "A network error occured while uploading this file" +msgstr "حدث خطأ في الشبكة أثناء تحميل هذا الملف" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "أدخِل إسم فنان …" + +#: front/src/components/library/Track.vue:195 +#: src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "المَقطَع" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "الرئيسية" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "إظهار/إخفاء الكلمة السرية" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "The Beatles, Mickael Jackson…" + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "The White Album, Thriller…" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "استخدم علبة التعليق هذه، إن دعت الحاجة، لإضافة تفاصيل طلبك" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" msgstr[0] "" @@ -2533,3 +2414,225 @@ msgstr[2] "" msgstr[3] "" msgstr[4] "" msgstr[5] "" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "فنان ، ألبوم ، مقطع موسيقي ..." + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "البحث عن فنانين أو ألبومات أو مَقاطِع موسيقية …" + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "تم خلط قائمة الإنتظار !" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "المَقطَع السابق" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "إعزف المَقطَع" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "ألبِث المَقطَع" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "المَقطَع التالي" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "إلغاء الكتم" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "كتم" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "" + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "" + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "" + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "خلط قائمة الإنتظار" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "مسح قائمة الانتظار الخاصة بك" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "طلبات الإستيراد المعلّقة" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "طلبات المتابَعة المعلَّقة" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "أدخِل طلب بحثِك …" + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "البحث حسب العنوان أو إسم فنان أو نطاق …" + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "أتركه فارغًا للحصول على رمز عشوائي" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "البحث باسم مستخدِم أو عنوان بريد إلكتروني أو رمز …" + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "البحث باسم مستخدِم أو عنوان بريد إلكتروني أو إسم …" + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "الفديرالية" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "" + +#: front/src/views/content/remote/ScanForm.vue:48 +msgid "Enter a library url" +msgstr "أدخِل عنوان رابط لمكتبة ما" + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "بدأ الإستكشاف" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:226 +msgid "Search by title, artist, album..." +msgstr "البحث حسب العنوان أو إسم فنان أو ألبوم…" + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "إنّ المقطع موجود مِن قبل في إحدى مكتباتك" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:242 +msgid "Import went on successfully" +msgstr "تمت عملية الإستيراد بنجاح" + +#: front/src/views/content/libraries/FilesTable.vue:259 +msgid "Relaunch import" +msgstr "إعادة محاولة الإستيراد" + +#: front/src/views/content/libraries/Card.vue:58 +msgid "Visibility: nobody except me" +msgstr "المشاهدة: لا أحد غيري" + +#: front/src/views/content/libraries/Card.vue:59 +msgid "Visibility: everyone on this instance" +msgstr "المشاهدة: كل مَن هم على مثيل الخادوم هذا" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:61 +msgid "Total size of the files in this library" +msgstr "الحجم الإجمالي للملفات المتوفّرة في هذه المكتبة" + +#: front/src/views/content/libraries/Form.vue:70 +msgid "My awesome library" +msgstr "مكتبتي الرائعة" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "تجتوي هذه المكتبة على الموسيقى الخاصة بي، أتمنى أنها ستلقى إعجابك!" + +#: front/src/views/content/libraries/Form.vue:74 +msgid "Everyone, including other instances" +msgstr "جميع المستخدمين بما في ذلك المتواجدون على مثيلات الخوادم الأخرى" + +#: front/src/views/content/libraries/Form.vue:106 +msgid "Library updated" +msgstr "تم تحديث المكتبة" + +#: front/src/views/content/libraries/Form.vue:109 +msgid "Library created" +msgstr "تم انشاء المكتبة" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "إضافة المحتوى و إدارته" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "الإذاعة" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "الخيط الزمني لمثيل الخادوم" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "قائمة المَقاطِع" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "أدخِل إسم قائمة مَقاطِعٍ موسيقية …" + +#: front/src/views/admin/library/Base.vue:16 +msgid "Manage library" +msgstr "إدارة المكتبة" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "" + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "" + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "إدارة المستخدِمين" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "إعدادات مثيل الخادوم" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "معلومات عن مثيل الخادوم" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "صاب سونيك" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "الإحصائيات" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "سِجِل الأخطاء" diff --git a/front/locales/de/LC_MESSAGES/app.po b/front/locales/de/LC_MESSAGES/app.po index 56926040c..ab1802d46 100644 --- a/front/locales/de/LC_MESSAGES/app.po +++ b/front/locales/de/LC_MESSAGES/app.po @@ -7,9 +7,9 @@ msgid "" msgstr "" "Project-Id-Version: front 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-22 14:12+0200\n" -"PO-Revision-Date: 2018-07-27 15:49+0000\n" -"Last-Translator: jovuit \n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" +"PO-Revision-Date: 2018-09-22 11:45+0000\n" +"Last-Translator: Gregory \n" "Language-Team: none\n" "Language: de\n" "MIME-Version: 1.0\n" @@ -30,14 +30,16 @@ msgstr "(%{ index } von %{ length })" msgid "(empty)" msgstr "(leer)" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "%{ count } von %{ total } ausgewählt" msgstr[1] "%{ count } von %{ total } ausgewählt" -#: front/src/components/Sidebar.vue:116 src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 +#: src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" msgstr[0] "%{ count } Track" @@ -49,11 +51,11 @@ msgid_plural "%{ count } tracks in %{ albumsCount } albums" msgstr[0] "%{ count } Track in %{ albumsCount } Alben" msgstr[1] "%{ count } Tracks in %{ albumsCount } Alben" -#: front/src/components/library/radios/Builder.vue:76 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" -msgstr[0] "%{ count } Track trifft die ausgewählten Filter" -msgstr[1] "%{ count } Tracks treffen die ausgewählten Filter" +msgstr[0] "%{ count } Track entspricht dem ausgewählten Filter" +msgstr[1] "%{ count } Tracks entspricht dem ausgewählten Filter" #: front/src/components/playlists/Card.vue:18 msgid "%{ count} track" @@ -61,6 +63,11 @@ msgid_plural "%{ count } tracks" msgstr[0] "%{ count} Track" msgstr[1] "%{ count} Tracks" +#: front/src/views/content/libraries/Quota.vue:11 +#, fuzzy +msgid "%{ current } used on %{ max } allowed" +msgstr "%{ count } von %{ total } ausgewählt" + #: front/src/components/common/Duration.vue:2 msgid "%{ hours } h %{ minutes } min" msgstr "%{ hours } h %{ minutes } min" @@ -77,10 +84,6 @@ msgstr "%{ user } hat einen Track zu seinen Favoriten hinzugefügt" msgid "%{ user } listened to a track" msgstr "%{ user } hat einen Track angehört" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "Profil von %{ username }" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" @@ -93,40 +96,30 @@ msgid_plural "%{ count } favorites" msgstr[0] "1 Favorit" msgstr[1] "%{ count } Favoriten" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "1 Track" -msgstr[1] "%{ count } Tracks" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "Über %{ instance }" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "Über Funkwhale" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "Über diese Instanz" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 -msgid "Accepted" -msgstr "Bestätigt" - -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "Zugriff ausgeschaltet" - -#: front/src/components/Home.vue:109 +#: front/src/views/content/libraries/Detail.vue:48 #, fuzzy +msgid "Accept" +msgstr "Akzeptiert" + +#: front/src/views/content/libraries/Detail.vue:40 +msgid "Accepted" +msgstr "Akzeptiert" + +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" -msgstr "" -"Greife deiner Musik zu durch eine klare Oberfläche, die auf das Wichtigste " -"fokusiert" +msgstr "Greife auf deine Musik mit einer übersichtliche Oberfläche zu, die sich auf das konzentriert, was wirklich wichtig ist" #: front/src/views/admin/users/UsersDetail.vue:54 msgid "Account active" @@ -136,30 +129,27 @@ msgstr "Konto aktiv" msgid "Account settings" msgstr "Kontoeinstellungen" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "Kontoeinstellungen" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "Kontostatus" #: front/src/views/auth/PasswordReset.vue:14 msgid "Account's email" -msgstr "Konto's Email" +msgstr "E-Mail Adresse des Kontos" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +#, fuzzy +msgid "Action" +msgstr "Aktionen" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" -msgstr[0] "" -"Die Aktion %{ action } wurde erfolgreich auf %{ count } Element gestartet" -msgstr[1] "" -"Die Aktion %{ action } wurde erfolgreich auf %{ count } Elemente gestartet" +msgstr[0] "Die Aktion %{ action } wurde erfolgreich für %{ count } Element gestartet" +msgstr[1] "Die Aktion %{ action } wurde erfolgreich für %{ count } Elemente gestartet" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:60 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "Aktionen" @@ -167,34 +157,26 @@ msgstr "Aktionen" msgid "Active" msgstr "Aktiv" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "Aktivität" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" -msgstr "Aktor" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" +msgstr "" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "Eine neue Mediathek einfügen" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" -msgstr "Filter einfügen" +msgstr "Filter hinzufügen" -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" -msgstr "Filter einfügen um deine Radio zu personalisieren" - -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "Zur Wiedergabeliste hinzufügen" +msgstr "Füge Filter hinzu, um dein Radio zu personalisieren" #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" -msgstr "Zu den Favoriten hinzufügen" +msgstr "Zu deinen Favoriten hinzufügen" #: front/src/components/playlists/TrackPlaylistIcon.vue:6 #: front/src/components/playlists/TrackPlaylistIcon.vue:32 @@ -205,10 +187,6 @@ msgstr "Zu einer Playlist hinzufügen..." msgid "Add to queue" msgstr "Zur Wiedergabeliste hinzufügen" -#: front/src/components/playlists/PlaylistModal.vue:116 -msgid "Add to this playlist" -msgstr "Zu dieser Playlist hinzufügen" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "Track hinzufügen" @@ -217,120 +195,89 @@ msgstr "Track hinzufügen" msgid "Admin" msgstr "Admin" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" -msgstr "Verwaltung" +msgstr "Administration" -#: front/src/components/audio/SearchBar.vue:26 src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 +#: src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "Album" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "Album %{ title } (%{ count } Track) von %{ artist }" -msgstr[1] "Album %{ title } (%{ count } Tracks) von %{ artist }" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" -msgstr[0] "Album mit %{ count } Track, von %{ artist }" +msgstr[0] "Album mit einem Track, von %{ artist }" msgstr[1] "Album mit %{ count } Tracks, von %{ artist }" #: front/src/components/library/Track.vue:20 msgid "Album page" msgstr "Albumseite" -#: front/src/components/audio/Search.vue:19 src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 +#: front/src/components/audio/Search.vue:19 +#: src/components/instance/Stats.vue:48 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "Alben" #: front/src/components/library/Artist.vue:44 msgid "Albums by this artist" -msgstr "Alben von diesem Künstler oder Künstlerin" +msgstr "Alben von diesem Künstler·in" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "Alles" #: front/src/components/playlists/Editor.vue:13 msgid "An error occured while saving your changes" -msgstr "Ein Fehler ist während der Speicherung deiner Veränderungen aufgetreten" +msgstr "Beim Speichern deiner Änderungen ist ein Fehler aufgetreten" #: front/src/components/auth/Login.vue:10 msgid "An unknown error happend, this can mean the server is down or cannot be reached" -msgstr "" -"Ein unbenkannter Fehler ist aufgetreten, d.h. der Server ist ausgescahltet " -"oder kann nicht erreicht werden" - -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "Alle" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "API" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "Zustimmen" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "Zugriff genehmigen?" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "Genehmigt" +msgstr "Ein unbekannter Fehler ist aufgetreten, vielleicht ist der Server ausgeschaltet oder er kann nicht erreicht werden" #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "Möchtest du dich wirklich ausloggen?" -#: front/src/components/audio/SearchBar.vue:25 src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 +#: src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" -msgstr "Künstler oder Künstlerin" +msgstr "Künstler·in" #: front/src/components/requests/Form.vue:5 +#: src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "Künstlername" -#: front/src/components/library/Album.vue:22 src/components/library/Track.vue:23 +#: front/src/components/library/Album.vue:22 +#: src/components/library/Track.vue:23 msgid "Artist page" msgstr "Künstlerseite" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "Künstler oder Künstlerin, Album, Track..." - -#: front/src/components/audio/Search.vue:10 src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 src/components/library/Library.vue:7 +#: front/src/components/audio/Search.vue:10 +#: src/components/instance/Stats.vue:42 +#: front/src/components/library/Library.vue:7 +#: src/components/library/Artists.vue:120 msgid "Artists" msgstr "Künstler und Künstlerinnen" #: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 src/components/library/Radios.vue:44 +#: src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" msgstr "Zunehmend" @@ -338,10 +285,6 @@ msgstr "Zunehmend" msgid "Ask for a password reset" msgstr "Eine Kennwortzurücksetzung beantragen" -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "Automatischer Import" - #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" msgstr "Verfügbare Playlists" @@ -355,40 +298,23 @@ msgstr "Avatar" msgid "Back to login" msgstr "Zurück zur Anmeldung" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "" -"Vorsicht beim Akzeptieren von Folgen-Anfragen: Deine Follower und " -"Followerinnen haben Zugriff auf deine gesamte Mediathek." - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "Bitrate" #: front/src/components/Sidebar.vue:18 src/components/library/Library.vue:4 msgid "Browse" -msgstr "Browsen" +msgstr "Durchsuchen" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "Die Mediathek durchsuchen" #: front/src/components/library/Artists.vue:4 msgid "Browsing artists" -msgstr "Künstler und Künstlerinnen durchsuchen" - -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "Föderierte Tracks durchsuchen" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "Follower und Followerinnen durchsuchen" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "Mediatheke durchsuchen" +msgstr "Künstler·innen durchsuchen" #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" @@ -406,25 +332,17 @@ msgstr "Editor" msgid "By %{ artist }" msgstr "Von %{ artist }" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." msgstr "" -"Beim Bestätigen wird %{ username } der Zugriff zu deiner Mediathek abgelehnt." -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "" -"Beim Bestätigen wird %{ username } der Zugriff zu deiner Mediathek " -"zugelassen." - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" -msgstr "Absagen" +msgstr "Abbrechen" -#: front/src/components/library/radios/Builder.vue:59 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "Kandidaten" @@ -432,7 +350,7 @@ msgstr "Kandidaten" msgid "Cannot change your password" msgstr "Das Kennwort kann nicht geändert werden" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" msgstr "Sprache ändern" @@ -459,15 +377,13 @@ msgstr "Änderungen synchronisiert" #: front/src/components/auth/Settings.vue:70 msgid "Changing your password will also change your Subsonic API password if you have requested one." -msgstr "" -"Mit Änderung deines Kennworts wird das Kennwort für die Subsonic-API " -"zurückgesetzt, sofern du eins erstellt hast." +msgstr "Mit Änderung deines Kennworts wird das Kennwort für die Subsonic-API zurückgesetzt, sofern du eins erstellt hast." #: front/src/components/auth/Settings.vue:98 msgid "Changing your password will have the following consequences" msgstr "Dein Kennwort wechseln hat die folgenden Auswirkungen" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "Wähle deine Instanz aus" @@ -482,24 +398,15 @@ msgstr "Löschen" #: front/src/components/playlists/Editor.vue:40 #: front/src/components/playlists/Editor.vue:45 msgid "Clear playlist" -msgstr "Playlist entleeren" - -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "Wiedergabeliste entleeren" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "CLI" +msgstr "Playlist leeren" #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" msgstr "Einmal clicken, und Musik studenlang dank der Radios anhören" -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" -msgstr "Geschlossen" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" +msgstr "" #: front/src/components/manage/users/InvitationForm.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:42 @@ -512,14 +419,13 @@ msgstr "Kode" msgid "Collapse" msgstr "Minimieren" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "Kommentar" -#: front/src/components/library/radios/Builder.vue:58 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" -msgstr "Einstellung" +msgstr "Einstellungen" #: front/src/components/common/DangerousButton.vue:21 msgid "Confirm" @@ -528,38 +434,46 @@ msgstr "Bestätigen" #: front/src/views/auth/EmailConfirm.vue:4 src/views/auth/EmailConfirm.vue:20 #: front/src/views/auth/EmailConfirm.vue:51 msgid "Confirm your email" -msgstr "Deine Emailadresse bestätigen" +msgstr "Bestätige deine E-Mail-Adresse" #: front/src/views/auth/EmailConfirm.vue:13 msgid "Confirmation code" msgstr "Bestätigungskode" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" -msgstr "Die Wiedergabeliste zur Playlist hinzufügen" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" +msgstr "" -#: front/src/components/Home.vue:88 -#, fuzzy +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" -msgstr "Songtexte, Album-Covers, wir möchten alles haben ;)" +msgstr "Albumcover, Songtexte, unser Ziel ist es, alle zu haben ;)" #: front/src/components/auth/Signup.vue:4 msgid "Create a funkwhale account" msgstr "Sich anmelden" +#: front/src/views/content/libraries/Home.vue:14 +#, fuzzy +msgid "Create a new library" +msgstr "Eine neue Playlist erstellen" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "Eine neue Playlist erstellen" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "Konto erstellen" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "Import erstellen" +#: front/src/views/content/libraries/Form.vue:26 +#, fuzzy +msgid "Create library" +msgstr "Eine hochwertige Mediathek" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "Mich anmelden" @@ -572,9 +486,8 @@ msgstr "Eine Playlist erstellen" msgid "Create your own radio" msgstr "Dein eigenes Radio erstellen" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "Erstelldatum" @@ -582,17 +495,37 @@ msgstr "Erstelldatum" msgid "Current avatar" msgstr "Aktuelles Avatar" +#: front/src/views/content/libraries/DetailArea.vue:4 +#, fuzzy +msgid "Current library" +msgstr "Eine hochwertige Mediathek" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "Aktueller Track" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +#, fuzzy +msgid "Current usage" +msgstr "Aktueller Track" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:29 +#: src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "Löschen" +#: front/src/views/content/libraries/Form.vue:39 +#, fuzzy +msgid "Delete library" +msgstr "Eine hochwertige Mediathek" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "Playlist löschen" @@ -601,53 +534,54 @@ msgstr "Playlist löschen" msgid "Delete radio" msgstr "Radio löschen" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" -msgstr "Ablehnen" - -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "Zugriff ablehnen?" +#: front/src/views/content/libraries/Form.vue:31 +#, fuzzy +msgid "Delete this library?" +msgstr "Bring mich zur Mediathek" #: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 src/components/library/Radios.vue:47 +#: src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "Absteigend" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +#, fuzzy +msgid "Description" +msgstr "Dauer" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "Detail" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." -msgstr "" -"Bestimmt, ob das Konto aktiv ist oder nicht. Inaktive Konten können sich " -"weder einloggen noch den Service nutzen." +#: front/src/views/content/remote/Card.vue:50 +#, fuzzy +msgid "Details" +msgstr "Detail" #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 msgid "Disable access" -msgstr "Zugriff ausschalten" +msgstr "Zugriff deaktivieren" #: front/src/components/auth/SubsonicTokenForm.vue:49 msgid "Disable Subsonic access" -msgstr "Subsonic-Zugriff ausschalten" +msgstr "Subsonic-Zugriff deaktivieren" #: front/src/components/auth/SubsonicTokenForm.vue:50 msgid "Disable Subsonic API access?" -msgstr "Subsonic-API-Zugriff ausschalten?" +msgstr "Subsonic-API-Zugriff deaktivieren?" #: front/src/components/auth/SubsonicTokenForm.vue:14 msgid "Discover how to use Funkwhale from other apps" msgstr "Entdecke, wie du Funkwhale von anderen Apps benutzen kannst" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "Öffentlich zeigen" @@ -673,40 +607,44 @@ msgid_plural "Do you want to launch %{ action } on %{ count } elements?" msgstr[0] "Möchtest du %{ action } auf %{ count } Element ausführen?" msgstr[1] "Möchtest du %{ action } auf %{ count } Elemente ausführen?" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "Möchtest du die Wiedergabelliste zurückerstellen?" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "Dokumentation" -#: front/src/components/audio/track/Table.vue:24 src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "Herunterladen" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "Tracks herunterladen" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" msgstr "Um die Playlist umzuordnen, klicke und schiebe die Tracks hin und her" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 +#: src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "Dauer" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "Leicht zu bedienen" +#: front/src/views/content/libraries/Detail.vue:9 +#, fuzzy +msgid "Edit" +msgstr "Bearbeiten..." + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "Die Infos dieser Instanz bearbeiten" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "Bearbeiten..." @@ -727,69 +665,27 @@ msgstr "E-Mail-Adresse bestätigt" msgid "End edition" msgstr "Bearbeitung abschließen" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "" -"Stelle sicher, dass deine Dateien angemessen verschlagwortet sind bevor du " -"sie hochlädst." - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "Name des Radios eingeben..." - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "Künstlername eingeben..." - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "Domainname eingeben..." - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "Playlistname eingeben..." - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "E-Mail-Adresse eingeben" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "" -"Gib deinen Einladungskode ein (Groß- und Kleinschreibung wird nicht " -"berücksichtigt)" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "Suche eingeben..." - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "Benutzername eingeben" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "Benutzername oder E-Mail-Adresse eingeben" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "Fehler" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" +#: front/src/views/content/remote/Card.vue:39 +#, fuzzy +msgid "Error during scan" msgstr "Fehlerbericht" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "Fehler bei der Ausführung der Aktion" #: front/src/views/auth/PasswordReset.vue:7 msgid "Error while asking for a password reset" -msgstr "Fehler während der Zurücksetzung des Passworts" +msgstr "Fehler bei der Zurücksetzung des Kennworts" #: front/src/views/auth/PasswordResetConfirm.vue:7 msgid "Error while changing your password" -msgstr "Fehler bei der Veränderung deines Passworts" +msgstr " Fehler bei der Änderung deines Passworts" #: front/src/views/auth/EmailConfirm.vue:7 msgid "Error while confirming your email" @@ -799,29 +695,31 @@ msgstr "Fehler bei der Bestätigung deiner E-Mail-Adresse" msgid "Error while creating invitation" msgstr "Fehler bei der Erstellung der Einladung" +#: front/src/views/content/remote/ScanForm.vue:3 +#, fuzzy +msgid "Error while fetching remote library" +msgstr "Fehler beim Scannen der Mediathek" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "Fehler bei der Speicherung der Einstellungen" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "Fehler beim Scannen der Mediathek" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "Ein Fehler ist aufgetreten" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" -msgstr "Alle" +#: front/src/views/content/libraries/Quota.vue:75 +#, fuzzy +msgid "Errored files" +msgstr "Ein Fehler ist aufgetreten" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" -msgstr "Alle auf dieser Instanz" +#: front/src/views/content/remote/Card.vue:58 +#, fuzzy +msgid "Errored tracks:" +msgstr "Föderierte Tracks" -#: front/src/components/library/radios/Builder.vue:57 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "Ausschließen" @@ -830,6 +728,7 @@ msgid "Expand" msgstr "Öffnen" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "Ablaufdatum" @@ -841,92 +740,51 @@ msgstr "Abgelaufen" msgid "Expired/used" msgstr "Abgelaufen bzw. benutzt" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "Externe Quelle. Unterstützte Systeme" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "Favoriten" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "Mit einer neuen Instanz föderieren" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "Föderierte Tracks" - -#: front/src/components/Sidebar.vue:87 src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "Föderation" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "Dateispiegelung" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "Dateiname" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "Datei hochladen" - -#: front/src/views/admin/library/Base.vue:5 src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/library/Base.vue:5 +#: src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "Dateien" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "nach Albumtyp filtern" - -#: front/src/components/library/radios/Builder.vue:56 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "Filtername" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "Import beenden" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "Beendet" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "Bitte zuerst auswählen, woher die Musik importiert werden muss" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "Folgen" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" +#: front/src/views/content/remote/Card.vue:88 +#, fuzzy +msgid "Follow pending approval" msgstr "Ausstehende Folgen-Anfrage" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" -msgstr "Folgenstatus" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" +msgstr "" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "Follower·innen" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "Nur Follower und Followerinnen" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "Abonniert" -#: front/src/components/activity/Like.vue:12 src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 +#: src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "aus %{ album } von %{ artist }" @@ -934,33 +792,25 @@ msgstr "aus %{ album } von %{ artist }" msgid "From album %{ album } by %{ artist }" msgstr "Aus dem Album %{ album } von %{ artist }" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" -msgstr "" -"Funkwhale ist ein kostenloses und Open-Source Projekt und wird von " -"ehrenamtlichen entwickelt. Hilf uns mit Fehler berichten, " -"Verbesserungsvorschlägen und teile das Projekt mit deinen Freunden!" +msgstr "Funkwhale ist ein kostenloses Open-Source Projekt, das von Ehrenamtlichen entwickelt wird. Hilf uns, indem du Fehler meldest, neue Funktionen vorschlägst und das Projekt mit deinen Freunden teilst!" #: front/src/components/auth/SubsonicTokenForm.vue:7 msgid "Funkwhale is compatible with other music players that support the Subsonic API." -msgstr "" -"Funkwhale ist kompatibel mit anderen Mediaplayer, die die Subsonic-API " -"unterstützen." +msgstr "Funkwhale ist kompatibel mit anderen Mediaplayer, die die Subsonic-API unterstützen." -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "Funkwhale ist sehr einfach zu benutzen." #: front/src/components/Home.vue:39 msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." -msgstr "" -"Funkwhale hat es leicht gemacht, deine Lieblingsmusik anzuhören und neue " -"Künstler und Künstlerinnen zu entdecken." +msgstr "Funkwhale hat es leicht gemacht, deine Lieblingsmusik anzuhören und neue Künstler und Künstlerinnen zu entdecken." -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." -msgstr "" -"Funkwhale ist kostenlos und ermöglicht eine komplett Steuerung deiner Musik." +msgstr "Funkwhale ist kostenlos und ermöglicht eine komplette Steuerung deiner Musik." #: front/src/components/Home.vue:66 msgid "Funkwhale takes care of handling your music" @@ -974,15 +824,15 @@ msgstr "Eine neue Einladung bekommen" msgid "Get me to the library" msgstr "Bring mich zur Mediathek" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " -msgstr "" -"Dank MusicBrainz " -"kannst du deine Musik mit hochwertigen Metadaten verschlagworten" +#: front/src/components/Home.vue:76 +#, fuzzy +msgid "Get quality metadata about your music thanks to MusicBrainz" +msgstr "Dank MusicBrainz kannst du deine Musik mit hochwertigen Metadaten verschlagworten" + +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +#, fuzzy +msgid "Get started" +msgstr "Nächster Schritt" #: front/src/components/common/ActionTable.vue:21 #: front/src/components/common/ActionTable.vue:27 @@ -993,17 +843,9 @@ msgstr "Los!" msgid "Go to home page" msgstr "Zurück zur Startseite" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "Entsprechende Metadaten aufrufen" - -#: front/src/App.vue:74 +#: front/src/App.vue:73 msgid "Help us translate Funkwhale" -msgstr "Hilf uns Funkwhale zu übersetzen" - -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "Start" +msgstr "Hilf uns, Funkwhale zu übersetzen" #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" @@ -1011,156 +853,48 @@ msgstr "Musik stundenlang" #: front/src/components/auth/SubsonicTokenForm.vue:11 msgid "However, accessing Funkwhale from those clients require a separate password you can set below." -msgstr "" -"Der Zugriff zu Funkwhale von anderen Softwares benötigt jedoch ein " -"zusätzliches Passwort. Du kannst dieses Passwort hier einstellen." - -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "ID" +msgstr "Für den Zugriff auf Funkwhale von anderen Apps ist ein zusätzliches Kennwort notwendig. Du kannst dieses Kennwort hier erstellen." #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." -msgstr "" -"Wenn die angegebene Emailadresse einem Benutzerkonto gebunden ist, wirst du " -"in Kürzen eine Email mit den Zurücksetzungsanleitungen bekommen." - -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "Import" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "Import #%{ id } gestartet" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "Import %{ count } Track" -msgstr[1] "Import %{ count } Tracks" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "Importstapel" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "Importstapel #%{ id }" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "Importstapel" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "Importstapel" +msgstr "Wenn die angegebene E-Mail-Adresse mit einem Benutzerkonto verknüpft ist, wirst du in Kürze eine E-Mail mit einer Anleitung zur Rücksetzung deines Passworts bekommen." #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "Importsdatum" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "Importdetails" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "Musik importieren" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "Musik importieren" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" -msgstr "Importiere Musik aus mehreren Plattformen, wie YouTube oder Soundcloud" +msgstr "Importiere Musik aus mehreren Plattformen, wie YouTube oder SoundCloud" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" -msgstr "Ausstehend" - -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "Importanfrage" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" +#: front/src/components/library/FileUpload.vue:51 +#, fuzzy +msgid "Import reference" msgstr "Importquelle" -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "Importstatus" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "Dieses Album importieren" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "Dieser Track importieren" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "Importiert" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "Importierte URL" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "Imports" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "In den Favoriten" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "In der Mediathek" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "Inaktiv" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "MusicBrainz-ID per Hand eingeben:" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "Die Emailadresse deines Kontos eingeben" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" msgstr[0] "Aus der Wiedergabeliste hinzufügen (%{ count } Track)" msgstr[1] "Aus der Wiedergabeliste hinzufügen (%{ count } Tracks)" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "Infos über diese Instanz" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "Radios der Instanz" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "Instanzeinstellungen" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "Aktivität der Instanz" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1170,55 +904,49 @@ msgstr "Einladungskode" msgid "Invitation code (optional)" msgstr "Einladungskode (ggf.)" -#: front/src/views/admin/users/Base.vue:8 src/views/admin/users/InvitationsList.vue:3 +#: front/src/views/admin/users/Base.vue:8 +#: src/views/admin/users/InvitationsList.vue:3 #: front/src/views/admin/users/InvitationsList.vue:24 msgid "Invitations" msgstr "Einladungen" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "Bugtracker" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "Aufgaben-ID" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" -msgstr "Aufgaben" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" +msgstr "" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" msgstr "Behalte einen Überblick auf deine Lieblingsmusik" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." -msgstr "" -"Halte deinen PRIVATE_TOKEN geheim, denn es gibt Zugriff zu deinem Konto." +#: front/src/views/content/remote/Home.vue:14 +#, fuzzy +msgid "Known libraries" +msgstr "Mediatheke durchsuchen" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "Letzte Aktivität" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "Letzter Abruf" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "Letzte Bearbeitung" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +#, fuzzy +msgid "Last update:" +msgstr "Playlist aktualisiert" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "Starten" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "Startdatum" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" msgstr "Scan starten" @@ -1226,182 +954,141 @@ msgstr "Scan starten" msgid "Learn more about this instance" msgstr "Mehr über diese Instanz erfahren" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "Leerlassen für einen beliebigen Kode" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "Lasse dieses Feld leer, wenn du die ganze Diskografie anfragst." -#: front/src/views/federation/Base.vue:5 src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "Mediatheke" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "" + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "Mediathek" #: front/src/views/admin/library/FilesList.vue:3 msgid "Library files" -msgstr "Datei der Mediathek" +msgstr "Dateien aus der Mediathek" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "Mediatheksname" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "Mediatheksgröße" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "library@demo.funkwhale.audio" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "Links" +#: front/src/views/content/libraries/Detail.vue:21 +#, fuzzy +msgid "Loading followers..." +msgstr "Follower und Followerinnen durchsuchen" + +#: front/src/views/content/libraries/Home.vue:3 +#, fuzzy +msgid "Loading Libraries..." +msgstr "Laden deiner Favoriten..." + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +#, fuzzy +msgid "Loading library data..." +msgstr "Domainname eingeben..." + +#: front/src/views/Notifications.vue:4 +#, fuzzy +msgid "Loading notifications..." +msgstr "Laden der Timeline..." + +#: front/src/views/content/remote/Home.vue:3 +#, fuzzy +msgid "Loading remote libraries..." +msgstr "Laden der Timeline..." + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "Laden der Timeline..." +#: front/src/views/content/libraries/Quota.vue:4 +#, fuzzy +msgid "Loading usage data..." +msgstr "Laden deiner Favoriten..." + #: front/src/components/favorites/List.vue:5 msgid "Loading your favorites..." msgstr "Laden deiner Favoriten..." -#: front/src/components/auth/Login.vue:78 -msgid "Log In" -msgstr "Einloggen" - #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "Logge dich zu deinem Funkwhale-Konto ein" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "Ausloggen" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "Als %{ username } angemeldet" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "Einloggen" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "Ausloggen" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "" -"Wiederholung deaktiviert. Schalte die Wiederholung des aktuellen Tracks beim " -"Klicken." - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "" -"Wiederholung des aktuellen Tracks. Wiederhole die ganze Wiedergabeliste beim " -"Klicken." - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "" -"Wiederholung auf die ganze Wiedergabeliste. Deaktiviere die Wiederholung " -"beim Klicken." - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "Liedtext" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "Mediathek verwalten" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "Playlists verwalten" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "Benutzer verwalten" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "Verwalte deine Playlists" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" -msgstr "Geschlossen markieren" - -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" +#: front/src/views/Notifications.vue:17 +#, fuzzy +msgid "Mark all as read" msgstr "Als importiert markieren" -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "Metadaten" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" msgstr "" -"Alle Infos über Alben, Tracks, Künstler und Künstlerinnen sind in den " -"Metadaten gespeichert. Hochwertige Metadaten können auf MusicBrainz geholt werden. " -"Das Projekt wird oft als Wikipedia für die Musik bezeichnet." -#: front/src/components/Sidebar.vue:48 src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "Musik" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "Musikanfrage" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "Stummschalten" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "Mein Konto" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" -msgstr "Meine super Playlist" +#: front/src/views/content/libraries/Home.vue:6 +#, fuzzy +msgid "My libraries" +msgstr "Mediatheke" -#: front/src/components/library/radios/Builder.vue:227 -msgid "My awesome radio" -msgstr "Mein super Radio" - -#: front/src/components/library/Track.vue:64 src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 +#: src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 +#: src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "k.A." #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "Name" @@ -1410,49 +1097,41 @@ msgstr "Name" msgid "New password" msgstr "Neues Kennwort" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "Neue Tracks werden hier automatisch hinzugefügt." -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "Nächster Schritt" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "Nächster Track" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "Nein" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" -msgstr "" -"Keine Add-Ons, keine Plugins: du brauchst nur eine Mediathek auf dem Internet" +msgstr "Keine Erweiterungen, keine Plugins: du brauchst nur eine online Mediathek" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "Kein verfügbarer Songtext für diesen Track." -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" -msgstr "Niemand außer mir" +#: front/src/components/federation/LibraryWidget.vue:6 +#, fuzzy +msgid "No matching library." +msgstr "Domainname eingeben..." -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "Nicht abonniert" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" -msgstr "Nicht importiert" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" +msgstr "" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "Nicht verwendet" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +#, fuzzy +msgid "Notifications" +msgstr "Letzte Bearbeitung" + +#: front/src/App.vue:36 msgid "Official website" msgstr "Offizielle Webseite" @@ -1460,43 +1139,32 @@ msgstr "Offizielle Webseite" msgid "Old password" msgstr "Altes Kennwort" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "" -"Nachdem alle Dateien hochgeladen sind, einfach auf dem folgenden Taste " -"clicken um das Status des Imports nachzuprüfen." - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 msgid "Open" msgstr "Frei" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "Optionen" -#: front/src/components/library/import/Main.vue:93 -msgid "Or" -msgstr "Oder" - #: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 src/components/library/Radios.vue:33 +#: src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" -msgstr "" +msgstr "Sortierung" #: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 src/components/library/Radios.vue:41 +#: src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "Reihenfolge" @@ -1504,10 +1172,6 @@ msgstr "Reihenfolge" msgid "Owner" msgstr "Besitzer" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "Seite nicht gefunden" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "Seite nicht gefunden!" @@ -1516,42 +1180,26 @@ msgstr "Seite nicht gefunden!" msgid "Password" msgstr "Kennwort" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "Kennwort aktualisiert" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "Kennwort erfolgreich aktualisiert" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "Track pausen" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "Ausstehend" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" -msgstr "Warte auf Bestätigung" +msgstr "Steht auf Bestätigung aus" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" -msgstr "Ausstehende Follow-Anfrage" +#: front/src/views/content/libraries/Quota.vue:22 +#, fuzzy +msgid "Pending files" +msgstr "Ausstehend" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "Ausstehende Importanfragen" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "Ausstehende Anfragen" @@ -1560,13 +1208,14 @@ msgstr "Ausstehende Anfragen" msgid "Permissions" msgstr "Berechtigungen" -#: front/src/components/audio/PlayButton.vue:9 src/components/library/Track.vue:30 +#: front/src/components/audio/PlayButton.vue:9 +#: src/components/library/Track.vue:30 msgid "Play" msgstr "Abspielen" #: front/src/components/audio/album/Card.vue:50 -#: front/src/components/audio/artist/Card.vue:44 src/components/library/Album.vue:28 -#: front/src/views/playlists/Detail.vue:23 +#: front/src/components/audio/artist/Card.vue:44 +#: src/components/library/Album.vue:28 front/src/views/playlists/Detail.vue:23 msgid "Play all" msgstr "Alles abspielen" @@ -1574,10 +1223,6 @@ msgstr "Alles abspielen" msgid "Play all albums" msgstr "Alle Alben abspielen" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "Jetzt abspielen" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "Danach abspielen" @@ -1586,14 +1231,6 @@ msgstr "Danach abspielen" msgid "Play now" msgstr "Jetzt abspielen" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "Abspielen" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "Playlist" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1620,9 +1257,9 @@ msgstr "Playlist aktualisiert" msgid "Playlist visibility" msgstr "Sichtbarkeit der Playlist" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" msgstr "Playlists" @@ -1636,51 +1273,51 @@ msgstr "Bitte prüfen, dass dein Kennwort richtig ist" #: front/src/components/auth/Login.vue:9 msgid "Please double-check your username/password couple is correct" -msgstr "" -"Bitte prüfe, dass dein Benutzername und dein Kennwort miteinander stimmen" +msgstr "Bitte prüfe, dass dein Benutzername und dein Kennwort miteinander stimmen" #: front/src/components/auth/Settings.vue:46 msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." -msgstr "" -"PNG, GIF oder JPG. Max. 2 Mb. Das Bild wird ggf. auf 400x400 px verkleinert." +msgstr "PNG, GIF oder JPG. Max. 2 Mb. Das Bild wird ggf. auf 400x400 px verkleinert." -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "Vorheriger Schritt" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" -msgstr "Vorheriger Track" +#: front/src/components/library/FileUpload.vue:58 +#, fuzzy +msgid "Proceed" +msgstr "Jetzt einloggen" #: front/src/views/auth/EmailConfirm.vue:26 #: front/src/views/auth/PasswordResetConfirm.vue:31 msgid "Proceed to login" msgstr "Jetzt einloggen" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" -msgstr "Ausgabedatum" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" +msgstr "" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" -msgstr "Abfragevorlage" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" +msgstr "" #: front/src/components/Sidebar.vue:20 msgid "Queue" msgstr "Wiedergabeliste" -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "Wiedergabeliste gemischt!" - -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "Radio" - -#: front/src/components/library/radios/Builder.vue:226 -msgid "Radio Builder" -msgstr "Radioeditor" - #: front/src/components/library/radios/Builder.vue:15 msgid "Radio created" msgstr "Radio erstellt" @@ -1693,7 +1330,8 @@ msgstr "Radioname" msgid "Radio updated" msgstr "Radio aktualisiert" -#: front/src/components/library/Library.vue:10 src/components/library/Radios.vue:141 +#: front/src/components/library/Library.vue:10 +#: src/components/library/Radios.vue:142 msgid "Radios" msgstr "Radios" @@ -1713,33 +1351,40 @@ msgstr "Neulich zu den Favoriten hinzugefügt" msgid "Recently listened" msgstr "Neulich angehört" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "MusicBrainz-ID des Tracks" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" msgstr "Aktualisieren" -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "Abgelehnt" - #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" msgstr "Angemeldet seit %{ date }" #: front/src/components/auth/Signup.vue:9 msgid "Registration are closed on this instance, you will need an invitation code to signup." -msgstr "" -"Die Anmeldung auf dieser Instanz sind geschlossen, du brauchst einen " -"Einladungskode, wenn du dich anmelden möchtest." +msgstr "Die Registrierung neuer Benutzer ist geschlossen. Du brauchst einen Einladungscode, wenn du dich trotzdem registrieren möchtest." #: front/src/components/manage/users/UsersTable.vue:71 msgid "regular user" msgstr "Standardnutzende" +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "" + +#: front/src/views/content/remote/Home.vue:6 +#, fuzzy +msgid "Remote libraries" +msgstr "Bring mich zur Mediathek" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." +msgstr "" + #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" msgstr "Löschen" @@ -1748,10 +1393,6 @@ msgstr "Löschen" msgid "Remove avatar" msgstr "Avatar löschen" -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "Aus den Favoriten entfernen" - #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" @@ -1765,148 +1406,94 @@ msgstr "Ein neues Subsonic-API-Kennwort beantragen?" msgid "Request a password" msgstr "Kennwort beantragen" -#: front/src/App.vue:35 -msgid "Request music" -msgstr "Musik anfragen" - -#: front/src/views/library/MusicRequest.vue:4 src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "Musik anfragen" - #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" -msgstr "Anfrage geschickt!" - -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "Fehlgeschlagene Aufgaben erneut ausführen" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "Aufgaben erneut ausführen" +msgstr "Anfrage gesendet!" #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" msgstr "Kennwort zurücksetzen" -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "Ergebnis %{ current }/%{ total }" - -#: front/src/components/favorites/List.vue:38 src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/favorites/List.vue:38 +#: src/components/library/Artists.vue:30 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" msgstr "Ergebnisse pro Seite" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:29 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" msgstr "Speichern" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" -msgstr "Scan gestartet!" +#: front/src/views/content/remote/Card.vue:31 +#, fuzzy +msgid "Scan pending" +msgstr "Zunehmend" -#: front/src/components/federation/LibraryTrackTable.vue:5 -#: front/src/components/library/Artists.vue:10 src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 +#: front/src/views/content/remote/Card.vue:43 +#, fuzzy +msgid "Scanned successfully" +msgstr "Einstellungen erfolgreich aktualisiert." + +#: front/src/views/content/remote/Card.vue:47 +#, fuzzy +msgid "Scanned with errors" +msgstr "Änderungen synchronisiert" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" +msgstr "" + +#: front/src/components/library/Artists.vue:10 +#: src/components/library/Radios.vue:29 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 src/views/playlists/List.vue:13 +#: front/src/views/content/libraries/FilesTable.vue:5 +#: src/views/playlists/List.vue:13 msgid "Search" msgstr "Suchen" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "Suche die zuimportierende Entität:" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "Künstler, Künstlerin, Benutzername oder Komment suchen..." - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "Quelle suchen..." - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "Benutzer, Benutzerin oder Quelle suchen..." - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "Titel, Künstler, Künstlerin oder Domain suchen..." - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "Benutzername oder Domain suchen..." - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "Benutzername, E-Mail-Adresse oder Kode suchen..." - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "Benutzername, E-Mail-Adresse oder Name suchen..." - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." -msgstr "Künstler, Künstlerinnen, Alben oder Tracks suchen..." +#: front/src/views/content/remote/ScanForm.vue:9 +#, fuzzy +msgid "Search a remote library" +msgstr "Bring mich zur Mediathek" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" msgstr "Musik suchen" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" msgstr "Auf lyrics.wikia.com suchen" -#: front/src/components/library/Album.vue:33 src/components/library/Artist.vue:31 +#: front/src/components/library/Album.vue:33 +#: src/components/library/Artist.vue:31 #: front/src/components/library/Track.vue:40 msgid "Search on Wikipedia" msgstr "Auf Wikipedia suchen" -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "Suche" - #: front/src/views/admin/Settings.vue:15 msgid "Sections" msgstr "Abschnitte" -#: front/src/components/library/radios/Builder.vue:41 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" msgstr "Filter auswählen" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" msgstr[0] "1 Element auswählen" msgstr[1] "Alle %{ total } Elemente auswählen" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "Hochzuladende Dateien auswählen..." - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" msgstr "Nur die aktuelle Seite auswählen" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "Zuhochladende Dateien oder Quellen auswählen" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "Folgen-Anfrage schicken" - -#: front/src/components/Sidebar.vue:97 src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" msgstr "Einstellungen" @@ -1918,71 +1505,68 @@ msgstr "Einstellungen aktualisiert" msgid "Settings updated successfully." msgstr "Einstellungen erfolgreich aktualisiert." -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "Einstellungen..." - #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" msgstr "Link zum Teilen" +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +#, fuzzy +msgid "Sharing link" +msgstr "Link zum Teilen" + +#: front/src/components/audio/album/Card.vue:40 +#, fuzzy +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "%{ count } Track" +msgstr[1] "%{ count } Tracks" + #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" msgstr[0] "1 Album mehr zeigen" msgstr[1] "%{ count } Alben mehr zeigen" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "1 Track mehr zeigen" -msgstr[1] "%{ count } Tracks mehr zeigen" +#: front/src/views/Notifications.vue:10 +#, fuzzy +msgid "Show read notifications" +msgstr "Letzte Bearbeitung" -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" -msgstr "Passwort verstecken bzw. zeigen" - -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" msgstr "Ergebnisse %{ start } bis %{ end } von %{ total }" -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "Wiedergabeliste mischen" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "Anmeldung" - #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" msgstr "Anmeldung" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "" -"Kopiere einfach den folgenden Text in einem Terminal um das Herunterladen zu " -"starten." - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 +#: src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" msgstr "Größe" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 msgid "Skipped" msgstr "Ausgelassen" +#: front/src/views/content/libraries/Quota.vue:49 +#, fuzzy +msgid "Skipped files" +msgstr "Ausgelassen" + #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "Dir fehlt etwas in der Mediathek? Sage uns was du anhören möchtest!" @@ -1993,25 +1577,17 @@ msgstr "Sorry, wir haben keine passende Alben für deine Suche gefunden" #: front/src/components/audio/Search.vue:16 msgid "Sorry, we did not found any artist matching your query" -msgstr "" -"Entschuldigung, wir haben keine passenden Künstler oder Künstlerinnen für " -"deine Suche gefunden" +msgstr "Entschuldigung, wir haben keine passenden Künstler oder Künstlerinnen für deine Suche gefunden" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "Quelle" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "Quellcode" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "Quelcode (%{ version })" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" msgstr "Teammember" @@ -2020,26 +1596,11 @@ msgstr "Teammember" msgid "Start" msgstr "Starten" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "Hochladen starten" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "Statistik" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "Status" @@ -2047,15 +1608,11 @@ msgstr "Status" msgid "Stop" msgstr "Abbrechen" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" msgstr "Radio stoppen" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "Hochladen abbrechen" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" msgstr "Abschicken" @@ -2063,96 +1620,85 @@ msgstr "Abschicken" msgid "Submit another request" msgstr "Erneut anfragen" -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "Vorgeschlagen von" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "Subsonic" - #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" msgstr "Subsonic-API-Kennwort" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" -msgstr "Erfolg" - -#: front/src/App.vue:11 +#: front/src/App.vue:13 msgid "Suggested choices" msgstr "Empfehlungen" +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" +msgstr "" + #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "Synchronisierung der Änderungen auf dem Server..." +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." -msgstr "" -"So einfach ist es: wir liebten Grooveshark, doch wollen wir noch besser " -"machen." +msgstr "So einfach ist es: wir liebten Grooveshark, doch wollen wir noch besser machen." -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "Die Beatles, Michael Jackson…" - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." msgstr "Das Funkwhale-Logo wurde mit Lieb von Francis Gading erbracht." -#: front/src/components/Home.vue:124 -msgid "The plaform is free and open-source, you can install it and modify it without worries" +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." msgstr "" -"Die Plattform ist kostenlos und Open-Source, du kannst sie herunterladen, " -"installieren und anpassen ohne Sorge" + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "" + +#: front/src/components/Home.vue:121 +msgid "The plaform is free and open-source, you can install it and modify it without worries" +msgstr "Die Plattform ist kostenlos und Open-Source, du kannst sie herunterladen, installieren und anpassen ohne Sorge" #: front/src/components/auth/SubsonicTokenForm.vue:4 msgid "The Subsonic API is not available on this Funkwhale instance." msgstr "Die Subsonic-API ist auf dieser Instanz nicht verfügbar." -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" -msgstr "Das Weiße Album, Thriller…" - -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" msgstr "" -"Zur zeit ist es nicht möglich, mehrere Tracks aus Funkwhale als Zip Archiv " -"herunterzuladen. Dennoch kannst du Tracks mit Hilfe von Kommandozeilen (z.B. " -"cURL) leicht herunterladen." -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" msgstr "" -"Dieser Import wird mit der folgenden Anfrage verknüpft. Nachdem der Import " -"fertig ist, wird die Anfrage als erfüllt markiert." -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" -msgstr "Dies weist darauf hin, ob die Fernmediathek dir den Zugriff gewährt hat" +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" +msgstr "" + +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." +msgstr "" #: front/src/components/auth/Profile.vue:16 msgid "This is you!" msgstr "Das bist du!" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." -msgstr "" -"Durch diese Aktion können mehrere Elemente betroffen werden, prüfe bitte " -"nach, das du es wirklich willst." +msgstr "Durch diese Aktion können mehrere Elemente betroffen werden, prüfe bitte nach, das du es wirklich willst." -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" -msgstr "Dieser Track wurde nicht importiert und kann nicht abgespielt werden" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "" + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" +msgstr "" #: front/src/views/playlists/Detail.vue:37 msgid "This will completely delete this playlist and cannot be undone." -msgstr "" -"Die Playlist wird dauerhaft gelöscht und kann nicht zurückerstellt werden." +msgstr "Die Playlist wird dauerhaft gelöscht und kann nicht zurückerstellt werden." #: front/src/views/radios/Detail.vue:27 msgid "This will completely delete this radio and cannot be undone." @@ -2162,35 +1708,192 @@ msgstr "Das Radio wird dauerhaft gelöscht und kann nicht zurückerstellt werden msgid "This will completely disable access to the Subsonic API using from account." msgstr "Der Zugriff zur Subsonic-API von diesem Konto wird deaktiviert." -#: front/src/App.vue:137 +#: front/src/App.vue:162 src/components/About.vue:55 +#: src/components/Home.vue:154 front/src/components/PageNotFound.vue:33 +#: src/components/Sidebar.vue:203 front/src/components/Sidebar.vue:204 +#: src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 +#: src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 +#: src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 +#: src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 +#: src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 +#: src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 +#: src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 +#: src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 +#: src/components/audio/SearchBar.vue:27 front/src/components/auth/Login.vue:77 +#: src/components/auth/Login.vue:78 front/src/components/auth/Logout.vue:20 +#: src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 +#: src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 +#: src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 +#: src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 +#: src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 +#: src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 +#: src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 +#: src/views/auth/EmailConfirm.vue:51 front/src/views/auth/PasswordReset.vue:52 +#: src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +#: src/views/content/Base.vue:19 front/src/views/content/Home.vue:35 +#: src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 +#: src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 +#: src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 +#: src/views/instance/Timeline.vue:57 front/src/views/playlists/Detail.vue:90 +#: src/views/playlists/List.vue:104 front/src/views/playlists/List.vue:105 +#: src/views/radios/Detail.vue:80 +#, fuzzy msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "" -"Deine lokalen Daten werden gelöscht, und du wirst abgemeldet. Möchtest du " -"fortfahren?" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "Deine lokalen Daten werden gelöscht, und du wirst abgemeldet. Möchtest du fortfahren?" +msgstr[1] "Deine lokalen Daten werden gelöscht, und du wirst abgemeldet. Möchtest du fortfahren?" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." -msgstr "" -"Du wirst von den bestehenden Geräten abgemeldet, die dieses Passwort nutzen." +msgstr "Du wirst von den bestehenden Geräten abgemeldet, die dieses Passwort nutzen." #: front/src/components/playlists/Editor.vue:44 msgid "This will remove all tracks from this playlist and cannot be undone." +msgstr "Alle Tracks dieses Playlists werden dauerhauft gelöscht und können nicht zurückerstellt werden." + +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." msgstr "" -"Alle Tracks dieses Playlists werden dauerhauft gelöscht und können nicht " -"zurückerstellt werden." #: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 #: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 msgid "Title" msgstr "Titel" -#: front/src/components/audio/SearchBar.vue:27 src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" -msgstr "Track" - #: front/src/components/library/Track.vue:53 msgid "Track information" msgstr "Trackinformation" @@ -2205,15 +1908,11 @@ msgstr "Tracks" #: front/src/components/library/Album.vue:43 #: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 -#: front/src/views/radios/Detail.vue:34 +#: src/views/content/Base.vue:8 front/src/views/content/libraries/Detail.vue:8 +#: src/views/playlists/Detail.vue:50 front/src/views/radios/Detail.vue:34 msgid "Tracks" msgstr "Tracks" -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "Verfügbare Tracks in dieser Mediathek" - #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" msgstr "Tracks von diesem Künstler oder Künstlerin" @@ -2226,36 +1925,39 @@ msgstr "Tracks in den Favoriten" msgid "tracks listened" msgstr "Angehörte Tracks" -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "Scan auslösen" - +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" msgstr "Typ" +#: front/src/views/content/remote/Card.vue:100 +#: src/views/content/remote/Card.vue:105 +#, fuzzy +msgid "Unfollow" +msgstr "Folgen" + +#: front/src/views/content/remote/Card.vue:101 +#, fuzzy +msgid "Unfollow this library?" +msgstr "Bring mich zur Mediathek" + #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." -msgstr "" -"Leider wurde diese Seite von den Verwaltern dieser Instanz noch nicht " -"ausgefüllt." - -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "Unbekannt" +msgstr "Leider wurde diese Seite von den Verwaltern dieser Instanz noch nicht ausgefüllt." #: front/src/components/Home.vue:37 msgid "Unlimited music" msgstr "Unbegrenzte Musik" -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "Stummschaltung aufheben" - #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" msgstr "Avatar aktualisieren" +#: front/src/views/content/libraries/Form.vue:25 +#, fuzzy +msgid "Update library" +msgstr "Mediathek verwalten" + #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" msgstr "Playlist aktualisiert" @@ -2268,8 +1970,10 @@ msgstr "Einstellungen aktualisieren" msgid "Update your password" msgstr "Dein Kennwort aktualisieren" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" msgstr "Hochladen" @@ -2277,44 +1981,57 @@ msgstr "Hochladen" msgid "Upload a new avatar" msgstr "Neues Avatar hochladen" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" -msgstr "Hochgeladene Dateien oder externe Quelle" +#: front/src/views/content/Home.vue:6 +#, fuzzy +msgid "Upload audio content" +msgstr "Neues Avatar hochladen" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/views/content/libraries/FilesTable.vue:54 +#, fuzzy +msgid "Upload date" +msgstr "Hochladen" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:31 +#, fuzzy +msgid "Upload new tracks" +msgstr "Neues Avatar hochladen" + +#: front/src/views/admin/users/UsersDetail.vue:82 +#, fuzzy +msgid "Upload quota" +msgstr "Hochladen" + +#: front/src/components/library/FileUpload.vue:99 +#, fuzzy +msgid "Uploaded" +msgstr "Hochladen" + +#: front/src/components/library/FileUpload.vue:5 +#, fuzzy +msgid "Uploading" +msgstr "Hochladen..." + +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "Hochladen..." -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" msgstr "Eine andere Instanz benutzen" -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "" -"Nutze dieses Feld um weitere Informationen über deine Anfrage beizufügen" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "" -"Mit dieser Einstellung kannst du die Föderation an dieser Mediathek " -"umschalten" - #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." -msgstr "" -"Mit diesem Formular kannst du ein neues Kennwort beantragen. Wir schicken " -"dir eine E-Mail an die angegebene Adresse mit den Anleitungen." - -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "Scanne eine Instanz und föderiere damit." +msgstr "Mit diesem Formular kannst du ein neues Kennwort beantragen. Wir schicken dir eine E-Mail an die angegebene Adresse mit den Anleitungen." #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" msgstr "Verwendet" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" msgstr "Nutzende" @@ -2322,12 +2039,20 @@ msgstr "Nutzende" msgid "User activity" msgstr "Aktivität der Nutzende" +#: front/src/components/library/Album.vue:49 +#: src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +#, fuzzy +msgid "User libraries" +msgstr "Mediatheke" + #: front/src/components/library/Radios.vue:20 msgid "User radios" msgstr "Radios der Nutzende" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" msgstr "Benutzername" @@ -2339,20 +2064,32 @@ msgstr "Benutzername oder E-Mail-Adresse" msgid "users" msgstr "Nutzende" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 +#: src/views/admin/users/UsersList.vue:21 front/src/views/admin/Settings.vue:81 msgid "Users" msgstr "Nutzende" -#: front/src/components/library/Album.vue:37 src/components/library/Artist.vue:35 +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +#, fuzzy +msgid "View files" +msgstr "Dateien aus der Mediathek" + +#: front/src/components/library/Album.vue:37 +#: src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" msgstr "Auf MusicBrainz ansehen" +#: front/src/views/content/libraries/Form.vue:18 +#, fuzzy +msgid "Visibility" +msgstr "Sichtbarkeit der Playlist" + #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" msgstr "Dieser Track kann nicht zu einer Playlist hinzugefügt werden" @@ -2377,12 +2114,21 @@ msgstr "Dein Avatar kann nicht gespeichert werden" msgid "We cannot save your settings" msgstr "Deine Einstellungen können nicht gespeichert werden" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" msgstr "Weder verfolgen wir dich noch stören wir dich mit Werbung" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:40 +#, fuzzy +msgid "we recommend using Picard for that purpose" msgstr "Zu diesem Zweck wird Picard empfohlen." #: front/src/components/Home.vue:7 @@ -2397,29 +2143,15 @@ msgstr "Entschuldigung, die aufgerufene Seite existiert nicht:" msgid "We've received your request, you'll get some groove soon ;)" msgstr "Deine Anfrage wurde empfangen, du wirst bald von uns was hören ;)" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "Willkommen" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" msgstr "Willkommen auf Funkwhale" -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "Was sind Metadaten?" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "" -"Wenn angeschaltet, neue veröffentliche Tracks in dieser Bibliothek werden " -"automatisch importiert" - #: front/src/components/Home.vue:24 msgid "Why funkwhale?" msgstr "Warum Funkwhale?" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" msgstr "Ja" @@ -2427,85 +2159,490 @@ msgstr "Ja" msgid "Yes, log me out!" msgstr "Ja, logge mich aus!" +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" +msgstr "" + #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" msgstr "Du bist als %{ username } angemeldet" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." msgstr "" -"Du kannst auch diesen Schritt überspringen und die Metadaten per Hand " -"eingeben." -#: front/src/components/Home.vue:136 +#: front/src/components/Home.vue:133 msgid "You can invite friends and family to your instance so they can enjoy your music" -msgstr "" -"Du kannst Freunde und Familie auf deiner Instanz einladen, sodass sie deine " -"Musik genießen können" +msgstr "Du kannst Freunde und Familie auf deiner Instanz einladen, sodass sie deine Musik genießen können" #: front/src/components/library/radios/Builder.vue:7 msgid "You can use this interface to build your own custom radio, which will play tracks according to your criteria." -msgstr "" -"Dank dieser Schnittstelle kannst du dein eigenes Radio aufbauen, das die " -"entsprechenden Tracks abspielt." +msgstr "Dank dieser Schnittstelle kannst du dein eigenes Radio aufbauen, das die entsprechenden Tracks abspielt." #: front/src/components/auth/SubsonicTokenForm.vue:8 msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." -msgstr "" -"Damit kannst du deine Playlists und Musik offline genießen, zum Beispiel auf " -"deinem Smartphone bzw. Tablett." +msgstr "Damit kannst du deine Playlists und Musik offline genießen, zum Beispiel auf deinem Smartphone bzw. Tablett." -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" msgstr "Du hörst gerade ein Radio an" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" msgstr "Zum Fortfahren wähle bitte eine Instanz aus" +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." +msgstr "" + #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" -msgstr "" -"Du wirst von dieser Sitzung ausgeloggt und du musst dich mit deinem neuen " -"Kennwort einloggen" +msgstr "Du wirst von dieser Sitzung ausgeloggt und du musst dich mit deinem neuen Kennwort einloggen" #: front/src/components/auth/Settings.vue:71 msgid "You will have to update your password on your clients that use this password." -msgstr "" -"Du musst das Passwort auf deine verbundenen Geräte anpassen, die dieses " -"Passwort verwenden." - -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "Du wirst importieren:" +msgstr "Du musst das Passwort auf deine verbundenen Geräte anpassen, die dieses Passwort verwenden." #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." -msgstr "" -"Deine E-Mail-Adresse wurde bestätigt, jetzt kannst du den Service unbegrenzt " -"nutzen." +msgstr "Deine E-Mail-Adresse wurde bestätigt, jetzt kannst du den Service unbegrenzt nutzen." -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" -msgstr "Deine Favoriten" - -#: front/src/components/Home.vue:117 +#: front/src/components/Home.vue:114 msgid "Your music, your way" msgstr "Deine Musik, deine Weise" +#: front/src/views/Notifications.vue:7 +#, fuzzy +msgid "Your notifications" +msgstr "Letzte Bearbeitung" + #: front/src/views/auth/PasswordResetConfirm.vue:29 msgid "Your password has been updated successfully." msgstr "Dein Kennwort wurde erfolgreich aktualisiert." #: front/src/components/auth/Settings.vue:101 msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" -msgstr "" -"Dein Subsonic-Kennwort wird mit einem neuen beliebigen Kennwort ersetzt, und " -"du wirst auf allen aktuellen verbundenen Geräten ausgeloggt, die das alte " -"Kennwort nutzen" +msgstr "Dein Subsonic-Kennwort wird mit einem neuen beliebigen Kennwort ersetzt, und du wirst auf allen aktuellen verbundenen Geräten ausgeloggt, die das alte Kennwort nutzen" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +#, fuzzy +msgid "Activity visibility" +msgstr "Sichtbarkeit der Playlist" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 +#: src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "Niemand außer mir" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 +#: src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "Alle auf dieser Instanz" + +#: front/src/components/mixins/Translations.vue:18 +#, fuzzy +msgid "Accessed date" +msgstr "Zugriff deaktiviert" + +#: front/src/components/mixins/Translations.vue:19 +#, fuzzy +msgid "Modification date" +msgstr "Ablaufdatum" + +#: front/src/components/mixins/Translations.vue:20 +#, fuzzy +msgid "Imported date" +msgstr "Importsdatum" + +#: front/src/components/mixins/Translations.vue:22 +#, fuzzy +msgid "Track name" +msgstr "Track" + +#: front/src/components/mixins/Translations.vue:23 +#, fuzzy +msgid "Album name" +msgstr "Albumseite" + +#: front/src/components/mixins/Translations.vue:30 +#, fuzzy +msgid "Sign-up date" +msgstr "Anmeldung" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "Die Wiedergabeliste zur Playlist hinzufügen" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "Zu dieser Playlist hinzufügen" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "Meine super Playlist" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "Alle" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "Anmeldung" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "Gib deinen Einladungskode ein (Groß- und Kleinschreibung wird nicht berücksichtigt)" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "Benutzername eingeben" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "Gib deine E-Mail-Adresse ein" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "Kennwort aktualisiert" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "Zugriff deaktiviert" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "Benutzername oder E-Mail-Adresse eingeben" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "Einloggen" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "Profil von %{ username }" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "Ausloggen" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "Kontoeinstellungen" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "Aus den Favoriten entfernen" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "Deine Favoriten" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "Name des Radios eingeben..." + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "Radioeditor" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "Mein super Radio" + +#: front/src/components/library/radios/Builder.vue:236 +#, fuzzy +msgid "My awesome description" +msgstr "Mein super Radio" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "" + +#: front/src/components/library/FileUpload.vue:240 +#, fuzzy +msgid "A network error occured while uploading this file" +msgstr "Beim Speichern deiner Änderungen ist ein Fehler aufgetreten" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "Künstlername eingeben..." + +#: front/src/components/library/Track.vue:195 +#: src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "Track" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "Start" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "Passwort verstecken bzw. zeigen" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "Die Beatles, Michael Jackson…" + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "Das Weiße Album, Thriller…" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "Nutze dieses Feld um weitere Informationen über deine Anfrage beizufügen" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" msgstr[0] "1 Track wurde zur Wiedergabeliste hinzugefügt" msgstr[1] "%{ count } Tracks wurden zur Wiedergabeliste hinzugefügt" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "Künstler oder Künstlerin, Album, Track..." + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "Künstler, Künstlerinnen, Alben oder Tracks suchen..." + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "Wiedergabeliste gemischt!" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "Vorheriger Track" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "Abspielen" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "Track pausen" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "Nächster Track" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "Stummschaltung aufheben" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "Stummschalten" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "Wiederholung deaktiviert. Schalte die Wiederholung des aktuellen Tracks beim Klicken." + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "Wiederholung des aktuellen Tracks. Wiederhole die ganze Wiedergabeliste beim Klicken." + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "Wiederholung auf die ganze Wiedergabeliste. Deaktiviere die Wiederholung beim Klicken." + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "Wiedergabeliste mischen" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "Wiedergabeliste leeren" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "Ausstehende Importanfragen" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "Ausstehende Follow-Anfrage" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "Suche eingeben..." + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "Titel, Künstler, Künstlerin oder Domain suchen..." + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "Leerlassen für einen beliebigen Kode" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "Benutzername, E-Mail-Adresse oder Kode suchen..." + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "Benutzername, E-Mail-Adresse oder Name suchen..." + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "Föderation" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "" + +#: front/src/views/content/remote/ScanForm.vue:48 +#, fuzzy +msgid "Enter a library url" +msgstr "Domainname eingeben..." + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:226 +#, fuzzy +msgid "Search by title, artist, album..." +msgstr "Titel, Künstler, Künstlerin oder Domain suchen..." + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:242 +#, fuzzy +msgid "Import went on successfully" +msgstr "Kennwort erfolgreich aktualisiert" + +#: front/src/views/content/libraries/FilesTable.vue:259 +#, fuzzy +msgid "Relaunch import" +msgstr "Import beenden" + +#: front/src/views/content/libraries/Card.vue:58 +#, fuzzy +msgid "Visibility: nobody except me" +msgstr "Niemand außer mir" + +#: front/src/views/content/libraries/Card.vue:59 +#, fuzzy +msgid "Visibility: everyone on this instance" +msgstr "Alle auf dieser Instanz" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:61 +#, fuzzy +msgid "Total size of the files in this library" +msgstr "Verfügbare Tracks in dieser Mediathek" + +#: front/src/views/content/libraries/Form.vue:70 +#, fuzzy +msgid "My awesome library" +msgstr "Mein super Radio" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:74 +#, fuzzy +msgid "Everyone, including other instances" +msgstr "Alle auf dieser Instanz" + +#: front/src/views/content/libraries/Form.vue:106 +#, fuzzy +msgid "Library updated" +msgstr "Mediatheksname" + +#: front/src/views/content/libraries/Form.vue:109 +#, fuzzy +msgid "Library created" +msgstr "Mediatheksname" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "Radio" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "Aktivität der Instanz" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "Playlist" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "Playlistname eingeben..." + +#: front/src/views/admin/library/Base.vue:16 +#, fuzzy +msgid "Manage library" +msgstr "In der Mediathek" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "Bestimmt, ob das Konto aktiv ist oder nicht. Inaktive Konten können sich weder einloggen noch den Service nutzen." + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "" + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "Benutzer verwalten" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "Instanzeinstellungen" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "Infos über diese Instanz" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "Subsonic" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "Statistik" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "Fehlerbericht" diff --git a/front/locales/eo/LC_MESSAGES/app.po b/front/locales/eo/LC_MESSAGES/app.po index 0d6e688fb..d676cc85f 100644 --- a/front/locales/eo/LC_MESSAGES/app.po +++ b/front/locales/eo/LC_MESSAGES/app.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: front 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-22 14:12+0200\n" -"PO-Revision-Date: 2018-07-24 19:50+0000\n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" +"PO-Revision-Date: 2018-10-05 16:38+0000\n" "Last-Translator: Baptiste Gelez \n" "Language-Team: none\n" "Language: eo\n" @@ -30,15 +30,16 @@ msgstr "(%{index} da %{length})" msgid "(empty)" msgstr "(malplena)" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "Unu el %{total} estas selekta" msgstr[1] "%{count} el %{total} estas selektaj" -#: front/src/components/Sidebar.vue:116 -#: src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 +#: src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" msgstr[0] "%{count} kanto" @@ -50,7 +51,7 @@ msgid_plural "%{ count } tracks in %{ albumsCount } albums" msgstr[0] "%{count} kanto en %{albumsCount} albumo" msgstr[1] "%{count} kantoj en %{albumsCount} albumoj" -#: front/src/components/library/radios/Builder.vue:76 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" msgstr[0] "%{count} kanto kongruas kun la tutaj filtriloj" @@ -62,6 +63,10 @@ msgid_plural "%{ count } tracks" msgstr[0] "%{count} kanto" msgstr[1] "%{count} kantoj" +#: front/src/views/content/libraries/Quota.vue:11 +msgid "%{ current } used on %{ max } allowed" +msgstr "%{current} uzantas el %{max} rajtantas" + #: front/src/components/common/Duration.vue:2 msgid "%{ hours } h %{ minutes } min" msgstr "%{hours} h %{minutes} min" @@ -78,10 +83,6 @@ msgstr "%{user} stelumis kanton" msgid "%{ user } listened to a track" msgstr "%{user} aŭskultis kanton" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "Profilo de %{username}" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" @@ -94,35 +95,27 @@ msgid_plural "%{ count } favorites" msgstr[0] "Unu stelumo" msgstr[1] "%{count} stelumoj" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "Unu kanto" -msgstr[1] "%{count} kantoj" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "Pri %{instance}" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "Pri Funkwhale" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "Pri tiu instanco" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 +#: front/src/views/content/libraries/Detail.vue:48 +msgid "Accept" +msgstr "Akcepti" + +#: front/src/views/content/libraries/Detail.vue:40 msgid "Accepted" msgstr "akceptanta" -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "Atingo malaktivigas" - -#: front/src/components/Home.vue:109 +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" msgstr "Atingas vian muzikon kun pura interfaco ke koncentras ĝin sur gravaĵo" @@ -134,10 +127,6 @@ msgstr "Aktiva konto" msgid "Account settings" msgstr "Preferoj de via konto" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "Preferoj de via konto" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "Pozicio de via konto" @@ -146,16 +135,18 @@ msgstr "Pozicio de via konto" msgid "Account's email" msgstr "Retadreso de via konto" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +msgid "Action" +msgstr "Ago" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" msgstr[0] "Ago %{action} sukcese komenciĝis por %{count} ero" msgstr[1] "Ago %{action} sukcese komenciĝis por %{count} eroj" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:60 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "Agoj" @@ -163,30 +154,22 @@ msgstr "Agoj" msgid "Active" msgstr "Aktiva" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "Aktivo" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" -msgstr "Aktoro" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" +msgstr "Aldoni muzikon" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "Aldoni nova muzikejo" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" msgstr "Aldoni filtrilo" -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" msgstr "Aldonu filtriloj por agordi vian radion" -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "Aldoni al aktuala atendovico" - #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" @@ -201,10 +184,6 @@ msgstr "Aldoni al ludlisto…" msgid "Add to queue" msgstr "Aldoni al atendovico" -#: front/src/components/playlists/PlaylistModal.vue:116 -msgid "Add to this playlist" -msgstr "Aldoni al tiu ludlisto" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "Aldoni kanton" @@ -213,25 +192,19 @@ msgstr "Aldoni kanton" msgid "Admin" msgstr "Administranto" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" msgstr "Administrejo" -#: front/src/components/audio/SearchBar.vue:26 -#: src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 +#: src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "Albumo" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "Albumo %{title} (%{count} kanto) je %{artist}" -msgstr[1] "Albumo %{title} (%{count} kantoj) je %{artist}" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" @@ -244,7 +217,6 @@ msgstr "Albumpagô" #: front/src/components/audio/Search.vue:19 #: src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "Albumoj" @@ -253,8 +225,8 @@ msgstr "Albumoj" msgid "Albums by this artist" msgstr "Albumoj je ĉi-tiu artisto" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "Ĉia" @@ -266,45 +238,21 @@ msgstr "Eraro okazis kiam konservi viajn ŝanĝojn" msgid "An unknown error happend, this can mean the server is down or cannot be reached" msgstr "Nekonata eraro okazis, povus signifi ke la servilo paneas aŭ ne estas atingebla" -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "Ajn" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "API" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "Aprobi" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "Aprobi atingo?" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "Aprobanta" - #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "Ĉu vi vere volas elsaluti?" -#: front/src/components/audio/SearchBar.vue:25 -#: src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 +#: src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" msgstr "Artisto" #: front/src/components/requests/Form.vue:5 +#: src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "Nomo de artisto" @@ -313,37 +261,26 @@ msgstr "Nomo de artisto" msgid "Artist page" msgstr "Artistpaĝo" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "Artisto, albumo, kanto…" - #: front/src/components/audio/Search.vue:10 #: src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 -#: src/components/library/Library.vue:7 +#: front/src/components/library/Library.vue:7 +#: src/components/library/Artists.vue:120 msgid "Artists" msgstr "Artistoj" #: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 -#: src/components/library/Radios.vue:44 +#: src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 -#: src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" msgstr "Foste" #: front/src/views/auth/PasswordReset.vue:27 -#, fuzzy msgid "Ask for a password reset" -msgstr "Demandi pasvorto renuligado" - -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "Aŭtomata importado" +msgstr "Demandi pasvortrenuligadon" #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" @@ -358,12 +295,9 @@ msgstr "Avataro" msgid "Back to login" msgstr "Reiri al ensalutpaĝo" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "Zorgu kiam vi akceptas petoj da sekvado, ĉar ĝi signifas ke la sekvanto atingos vian tutan muzikejo." - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "Bitrapido" @@ -371,7 +305,7 @@ msgstr "Bitrapido" msgid "Browse" msgstr "Folii" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "Folii muzikejon" @@ -379,18 +313,6 @@ msgstr "Folii muzikejon" msgid "Browsing artists" msgstr "Folii artistojn" -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "Folii federantajn kantojn" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "Folii sekvadojn" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "Folii muzikejojn" - #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" msgstr "Folii ludlistojn" @@ -407,22 +329,17 @@ msgstr "Konstruilo" msgid "By %{ artist }" msgstr "Je %{artist}" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." -msgstr "Konfirmante, %{username} ne plu rajtos atingi vian muzikejo." +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." +msgstr "Malsekvante tiu muzikejo, vi perdos ĝian muzikon." -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "Konfirmante, %{username} rajtos atingi vian muzikejo." - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" msgstr "Nuligi" -#: front/src/components/library/radios/Builder.vue:59 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "Kandidatoj" @@ -430,7 +347,7 @@ msgstr "Kandidatoj" msgid "Cannot change your password" msgstr "Ne eblas ŝanĝi vian pasvorton" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" msgstr "Ŝanĝi lingvon" @@ -463,7 +380,7 @@ msgstr "Ŝanĝi vian pasvorton ankaŭ ŝanĝos vian Subsonic API pasvorto se vi msgid "Changing your password will have the following consequences" msgstr "Ŝanĝi vian pasvorton tiel rezultigos" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "Elekti vian instanco" @@ -480,22 +397,13 @@ msgstr "Purigi" msgid "Clear playlist" msgstr "Purigi ludliston" -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "Purigi vian atendovico" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "Komandlinia interfaco" - #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" msgstr "Alklaki unu foje, aŭskulti enkonstruitajn radiojn dum horoj" -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" -msgstr "Fermanta" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" +msgstr "Alklaki por selekti elŝutontaj dosieroj, aŭ ŝovi kaj demeti dosierojn aŭ dosierujojn" #: front/src/components/manage/users/InvitationForm.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:42 @@ -508,12 +416,11 @@ msgstr "Kodo" msgid "Collapse" msgstr "Malgrandigi" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "Komenti" -#: front/src/components/library/radios/Builder.vue:58 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" msgstr "Preferoj" @@ -530,11 +437,11 @@ msgstr "Konfirmi vian retadreson" msgid "Confirmation code" msgstr "Konfirmada kodo" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" -msgstr "Kopii kantojn el la aktuala atendovico en tiu ludlisto" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" +msgstr "Kopii" -#: front/src/components/Home.vue:88 +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" msgstr "Reludversioj, kantparoloj, nian celon estas havi ĉiujn ;)" @@ -542,19 +449,26 @@ msgstr "Reludversioj, kantparoloj, nian celon estas havi ĉiujn ;)" msgid "Create a funkwhale account" msgstr "Krei Funkwhale konton" +#: front/src/views/content/libraries/Home.vue:14 +msgid "Create a new library" +msgstr "Krei novan muzikejon" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "Krei novan ludliston" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "Krei konton" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "Krei importadon" +#: front/src/views/content/libraries/Form.vue:26 +msgid "Create library" +msgstr "Krei muzikejon" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "Kreu mian konton" @@ -567,9 +481,8 @@ msgstr "Krei ludliston" msgid "Create your own radio" msgstr "Krei vian propran radion" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "Kreodato" @@ -577,17 +490,34 @@ msgstr "Kreodato" msgid "Current avatar" msgstr "Aktuala avataro" +#: front/src/views/content/libraries/DetailArea.vue:4 +msgid "Current library" +msgstr "Aktuala muzikejo" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "Aktuala kanto" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +msgid "Current usage" +msgstr "Aktuala uzo" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "Dato" + +#: front/src/views/content/libraries/Form.vue:29 +#: src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "Forigi" +#: front/src/views/content/libraries/Form.vue:39 +msgid "Delete library" +msgstr "Forigi muzikejon" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "Forigi ludliston" @@ -596,34 +526,32 @@ msgstr "Forigi ludliston" msgid "Delete radio" msgstr "Forigi radion" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" -msgstr "Malrajtu" - -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "Malrajti atingado?" +#: front/src/views/content/libraries/Form.vue:31 +msgid "Delete this library?" +msgstr "Forigi tiun muzikejon?" #: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 -#: src/components/library/Radios.vue:47 +#: src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 -#: src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "Malfoste" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +msgid "Description" +msgstr "Resumo" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "Detalo" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." -msgstr "Elekti ĉu la konton aktivas aŭ ne. Malaktivaj konton ne eblas ensaluti aŭ uzi la servico." +#: front/src/views/content/remote/Card.vue:50 +msgid "Details" +msgstr "Detaloj" #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 @@ -642,7 +570,7 @@ msgstr "Ĉu vi volas malatingeblu la Subsonic API?" msgid "Discover how to use Funkwhale from other apps" msgstr "Malkovri kiel vi povas uzi Funkwhale el aliaj aplikaĵoj" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "Publike montri" @@ -659,9 +587,8 @@ msgid "Do you want to delete the playlist \"%{ playlist }\"?" msgstr "Ĉu vi volas forigi la \"%{playlist}\" ludliston?" #: front/src/views/radios/Detail.vue:26 -#, fuzzy msgid "Do you want to delete the radio \"%{ radio }\"?" -msgstr "Ĉu vi volas forigi la \"%{playlist}\" ludliston?" +msgstr "Ĉu vi volas forigi la \"%{radio}\" radion?" #: front/src/components/common/ActionTable.vue:29 msgid "Do you want to launch %{ action } on %{ count } element?" @@ -669,41 +596,43 @@ msgid_plural "Do you want to launch %{ action } on %{ count } elements?" msgstr[0] "Ĉu vi volas komenci %{action} por %{count} ero?" msgstr[1] "Ĉu vi volas komenci %{action} por %{count} eroj?" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "Ĉu vi volas reŝargi vian antaŭan atendovicon?" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "Dokumentaro" -#: front/src/components/audio/track/Table.vue:24 -#: src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "Elŝuti" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "Elŝuti kantojn" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" msgstr "Treni kaj guti horizontaloj por reordigi kantojn en la ludlisto" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 +#: src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "Daŭro" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "Facila uzo" +#: front/src/views/content/libraries/Detail.vue:9 +msgid "Edit" +msgstr "Redakti" + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "Redakti informoj de instanco" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "Redakti…" @@ -724,56 +653,16 @@ msgstr "Konfirmis retadreso" msgid "End edition" msgstr "Fini redakto" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "" -"Zorgu ke viajn muzikdosierojn estas korekte etikedanta antaŭ alŝutis ilin." - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "Tajpu nomon de radio…" - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "Tajpu nomon de artisto…" - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "Tajpu domajna nomo de instance…" - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "Tajpu ludlistan nomon…" - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "Tajpu vian retadreson" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "Tajpu vian invitkodon (usklecoblindan)" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "Tajpu vian serĉon…" - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "Tajpu vian uzantnomon" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "Tajpu vian uzantnomon aŭ retadreson" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "Eraro" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" -msgstr "Eraroraportado" +#: front/src/views/content/remote/Card.vue:39 +msgid "Error during scan" +msgstr "Eraro dum skanado" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "Eraro kiam ruli akton" @@ -793,29 +682,28 @@ msgstr "Eraro kiam konfirmi vian retadreson" msgid "Error while creating invitation" msgstr "Eraro kiam krei inviton" +#: front/src/views/content/remote/ScanForm.vue:3 +msgid "Error while fetching remote library" +msgstr "Eraro dum skano de malloka muzikejo" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "Eraro kiam konservi preferojn" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "Eraro kiam skani muzikejon" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "Eraris" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" -msgstr "Ĉiu" +#: front/src/views/content/libraries/Quota.vue:75 +msgid "Errored files" +msgstr "Erarintaj dosieroj" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" -msgstr "Ĉiu en ĉi-tiu instanco" +#: front/src/views/content/remote/Card.vue:58 +msgid "Errored tracks:" +msgstr "Erorintaj kantoj:" -#: front/src/components/library/radios/Builder.vue:57 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "Ekskluzivi" @@ -824,6 +712,7 @@ msgid "Expand" msgstr "Grandigi" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "Fortempiĝa dato" @@ -835,97 +724,50 @@ msgstr "Fortempiĝis" msgid "Expired/used" msgstr "Fortempiĝis aŭ uzantiĝis" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "Ekstera fonto. Kompateblaj servicoj" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "Stelumoj" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "Federi kun nova instanco" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "Federataj kantoj" - -#: front/src/components/Sidebar.vue:87 -#: src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 -#: src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 -#: src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "Federo" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "Dosierospegulado" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "Nomo de la dosiero" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "Dosiero alŝutado" - #: front/src/views/admin/library/Base.vue:5 #: src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "Dosieroj" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "Filtri albumtipoj" - -#: front/src/components/library/radios/Builder.vue:56 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "Filtri nomon" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "Fini importadon" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "Finanto" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "Unue, elekti ejo el vi volas importi muzikon" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "Sekvi" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" -msgstr "Peto da sekvado atendis konsenton" +#: front/src/views/content/remote/Card.vue:88 +msgid "Follow pending approval" +msgstr "Peto da sekvado atendas konsenton" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" -msgstr "Sekva statuso" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" +msgstr "Sekvi mallokajn muzikejojn" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "Sekvantoj" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "Nur sekvantoj" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "Sekvata" -#: front/src/components/activity/Like.vue:12 -#: src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 +#: src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "el %{album} je %{artist}" @@ -933,28 +775,23 @@ msgstr "el %{album} je %{artist}" msgid "From album %{ album } by %{ artist }" msgstr "El %{album} albumo je %{artist}" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" -msgstr "" -"Funkwhale estas senpaga kaj kun libera fontkodo projekto, ke viglas dankon " -"al vonlontuloj. Vi povas helpi nin plibonigi tiun kun cimosignaladoj, " -"trajtosugestoj kaj diskonigado de la projekto al viaj amikoj!" +msgstr "Funkwhale estas senpaga kaj kun libera fontkodo projekto, ke viglas dankon al vonlontuloj. Vi povas helpi nin plibonigi tiun kun cimosignaladoj, trajtosugestoj kaj diskonigado de la projekto al viaj amikoj!" #: front/src/components/auth/SubsonicTokenForm.vue:7 msgid "Funkwhale is compatible with other music players that support the Subsonic API." msgstr "Funkwhale funkcias kun aliaj muzikludiloj ke apogas la Subsonic API." -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "Uzi Funkwhale facilegas." #: front/src/components/Home.vue:39 msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." -msgstr "" -"Funkwhale estas dizajna tiel ke estas facile aŭskulti muzikon vi ŝatas, aŭ " -"malkovri novajn artistojn." +msgstr "Funkwhale estas dizajna tiel ke estas facile aŭskulti muzikon vi ŝatas, aŭ malkovri novajn artistojn." -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." msgstr "Funkwhale estas senpaga kaj lasis vin estri vian muzikon." @@ -970,15 +807,14 @@ msgstr "Akiri novan inviton" msgid "Get me to the library" msgstr "Iru al la muzikejo" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " -msgstr "" -"Akiri bonegaj metadatumoj pri vian muzikon kun MusicBrainz" +#: front/src/components/Home.vue:76 +#, fuzzy +msgid "Get quality metadata about your music thanks to MusicBrainz" +msgstr "Akiri bonegaj metadatumoj pri vian muzikon kun MusicBrainz" + +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +msgid "Get started" +msgstr "Komencu" #: front/src/components/common/ActionTable.vue:21 #: front/src/components/common/ActionTable.vue:27 @@ -989,17 +825,9 @@ msgstr "Komenci" msgid "Go to home page" msgstr "Iru hejme" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "Kolekti la koncernajn metadatumojn" - -#: front/src/App.vue:74 +#: front/src/App.vue:73 msgid "Help us translate Funkwhale" -msgstr "" - -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "Hejmo" +msgstr "Helpu nin traduki Funkwhale" #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" @@ -1007,156 +835,47 @@ msgstr "Muzikhoroj" #: front/src/components/auth/SubsonicTokenForm.vue:11 msgid "However, accessing Funkwhale from those clients require a separate password you can set below." -msgstr "" -"Tamen, atingi Funkwhale el tiuj aplikaĵo bezonas alian pasvorton ke vi povas " -"difini malsupre." - -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "ID" +msgstr "Tamen, atingi Funkwhale el tiuj aplikaĵo bezonas alian pasvorton ke vi povas difini malsupre." #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." -msgstr "" -"Se la retadreso provizanta dum la antaŭa etapo korektas kaj bindas al " -"uzantkonto, vi baldaŭ ricevus retmesaĝon kun renuligadaj instrukcioj." - -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "Importi" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "Importado #%{id} komencis" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "Importi unu kanto" -msgstr[1] "Importi %{count} kantoj" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "Importaro" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "Importaro #%{id}" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "Importaroj" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "Importaroj" +msgstr "Se la retadreso provizanta dum la antaŭa etapo korektas kaj bindas al uzantkonto, vi baldaŭ ricevus retmesaĝon kun renuligadaj instrukcioj." #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "Importdato" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "Importado detalpaĝo" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "Importi muzikon" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "Importi muzikon" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" msgstr "Importi muzikon el multe da servicoj, kiel YouTube aŭ SoundCloud" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" -msgstr "Importado atendas" - -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "Importpetoj" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" +#: front/src/components/library/FileUpload.vue:51 +msgid "Import reference" msgstr "Importfonto" -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "Importstato" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "Importi tiun albumon" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "Importi tiun kanton" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "Importata" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "Importanta URL" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "Importadoj" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "En stelumoj" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "En muzikejo" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "Malaktiva" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "Tajpu MusicBrainz ID mane:" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "Tajpu la retadreson bindanta al via konto" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" msgstr[0] "Internigi el atendovico (unu kanto)" msgstr[1] "Internigi el atendovico (%{count} kantoj)" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "Instanca informo" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "Instancaj radioj" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "Instancaj preferoj" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "Instanca tempolino" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1172,49 +891,41 @@ msgstr "Invita kodo (nedeviga)" msgid "Invitations" msgstr "Invitoj" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "Cimspuradilo" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "Labora ID" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" -msgstr "Laboroj" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" +msgstr "Ŝajni ke vi ne jam havas muzikejon, kreu unu!" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" msgstr "Konservi postsignojn de viaj preferitaj kantoj" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." -msgstr "Konservu vian PRIVATE_TOKEN sekreta, ĉar ĝi atingeblas vian konton." +#: front/src/views/content/remote/Home.vue:14 +msgid "Known libraries" +msgstr "Konataj muzikejoj" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "Lasta akto" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "Lasta kolektado" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "Lasta redakto" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +msgid "Last update:" +msgstr "Lasta ĝisdatigo:" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "Lanĉi" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "Lanĉa dato" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" msgstr "Lanĉi skanon" @@ -1222,25 +933,21 @@ msgstr "Lanĉi skanon" msgid "Learn more about this instance" msgstr "Lerni pli pri tiu instanco" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "Lasu malplena por hazarda kodo" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "Lasu malplena se vi volas la tutan albumaron." -#: front/src/views/federation/Base.vue:5 -#: src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "Muzikejoj" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "Muzikejoj helpas vin organizi kaj diskonigi viajn muzikarojn. Vi povas elŝuti vian propran muzikaron je Funkwhale kaj diskonigi ĝin kun viajn amikojn kaj familio." + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "Muzikejo" @@ -1248,155 +955,111 @@ msgstr "Muzikejo" msgid "Library files" msgstr "Muzikejaj dosieroj" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "Nomo de muzikejo" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "Muzikejgrando" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "library@demo.funkwhale.audio" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "Ligiloj" +#: front/src/views/content/libraries/Detail.vue:21 +msgid "Loading followers..." +msgstr "Ŝargi sekvantojn…" + +#: front/src/views/content/libraries/Home.vue:3 +msgid "Loading Libraries..." +msgstr "Ŝarĝas muzikejojn…" + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +msgid "Loading library data..." +msgstr "Ŝargas datumon de la muzkejo…" + +#: front/src/views/Notifications.vue:4 +msgid "Loading notifications..." +msgstr "Ŝarĝas sciigojn…" + +#: front/src/views/content/remote/Home.vue:3 +msgid "Loading remote libraries..." +msgstr "Ŝargas mallokajn muzikejojn…" + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "Tempolino ŝarĝas…" -#: front/src/components/favorites/List.vue:5 -#, fuzzy -msgid "Loading your favorites..." -msgstr "Ŝarĝantas stelumojn" +#: front/src/views/content/libraries/Quota.vue:4 +msgid "Loading usage data..." +msgstr "Ŝarĝas uzdatumon…" -#: front/src/components/auth/Login.vue:78 -msgid "Log In" -msgstr "Ensaluti" +#: front/src/components/favorites/List.vue:5 +msgid "Loading your favorites..." +msgstr "Ŝarĝas viajn stelumojn…" #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "Ensaluti en via Funkwhale konto" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "Elsaluti" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "Elsuta je %{username}" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "Ensaluti" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "Elsaluti" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "Ripeto malaktivas. Alklaki por aktivi ripetado de la aktuala kanto." - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "Ripetas unu kanton. Alklaki por aktivi ripetado de la tutan atendovico." - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "Ripetas la tutan atendovicon. Alklaki por malaktivi ripeto." - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "Teksto" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "Manipuli muzikejon" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "Manipuli ludlistojn" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "Manipuli uzantojn" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "Manipuli viajn ludlistojn" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" -msgstr "Marki fermata" +#: front/src/views/Notifications.vue:17 +msgid "Mark all as read" +msgstr "Marki ĉiujn legata" -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" -msgstr "Marki importata" +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" +msgstr "Mb" -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "Metadatumoj" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." -msgstr "" -"Metadatumoj estas datumoj rilatanta al muziko vi volas importi. Ĝi enhavas " -"ĉiu informo pri artistoj, albumoj kaj kantoj. Por havi bonega muzikejo, " -"estas rekomendata kolekti datumojn el la MusicBrainz projekto, ke similas al Vikipedio sed por " -"muziko." - -#: front/src/components/Sidebar.vue:48 -#: src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "Muziko" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "Muzikpeto" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "Silentigi" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "Mia konto" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" -msgstr "Mia mojosa ludlisto" +#: front/src/views/content/libraries/Home.vue:6 +msgid "My libraries" +msgstr "Miaj muzikejoj" -#: front/src/components/library/radios/Builder.vue:227 -msgid "My awesome radio" -msgstr "Mia mojosa radio" - -#: front/src/components/library/Track.vue:64 -#: src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 +#: src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 +#: src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "ND" #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "Nomo" @@ -1405,48 +1068,39 @@ msgstr "Nomo" msgid "New password" msgstr "Nova pasvorto" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "Novaj kantoj estos aldonataj ĉi-tie aŭtomate." -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "Baldaŭa etapo" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "Baldaŭa kanto" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "Ne" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" msgstr "Nek aldonaĵoj, nek kromprogramoj: vi nur bezonas retmuzikejo" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "Nenio teksto disponeblas por tiu kanto." -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" -msgstr "Neniu krom mi" +#: front/src/components/federation/LibraryWidget.vue:6 +msgid "No matching library." +msgstr "Neniu muzikejo korespondas." -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "Ne sekvas" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" -msgstr "Ne importintas" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" +msgstr "Neniu sekvas tiun muzikejon" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "Ne uzantata" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +msgid "Notifications" +msgstr "Sciigoj" + +#: front/src/App.vue:36 msgid "Official website" msgstr "Oficiala retejo" @@ -1454,47 +1108,32 @@ msgstr "Oficiala retejo" msgid "Old password" msgstr "Malnova pasvorto" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "" -"Kiam viaj dosieroj estas tute elŝutinta, ĵus alklaki tiu butono por vidi la " -"staton de la importo." - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 msgid "Open" msgstr "Malferma" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "Preferoj" -#: front/src/components/library/import/Main.vue:93 -msgid "Or" -msgstr "Aŭ" - #: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 -#: src/components/library/Radios.vue:33 +#: src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 -#: src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" msgstr "Ordo" #: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 -#: src/components/library/Radios.vue:41 +#: src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 -#: src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "Orda direkto" @@ -1502,10 +1141,6 @@ msgstr "Orda direkto" msgid "Owner" msgstr "Proprietulo" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "Ne eblas trovi tiun paĝon" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "Maltrovitas paĝon!" @@ -1514,42 +1149,25 @@ msgstr "Maltrovitas paĝon!" msgid "Password" msgstr "Pasvorto" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "Pasvorto aktuliginta" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "Pasvorto sukcese aktualiginta" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "Paŭzi kanton" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "Atendas" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" msgstr "Atendas aprobon" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" -msgstr "Atendantaj petoj da sekvado" +#: front/src/views/content/libraries/Quota.vue:22 +msgid "Pending files" +msgstr "Atendantaj dosieroj" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "Atendantaj importpetoj" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "Atendantaj petoj" @@ -1573,10 +1191,6 @@ msgstr "Ludi ĉiu" msgid "Play all albums" msgstr "Ludi ĉiuj albumoj" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "Ludi tuj" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "Ludi baldaŭe" @@ -1585,14 +1199,6 @@ msgstr "Ludi baldaŭe" msgid "Play now" msgstr "Ludi tuj" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "Ludi kanton" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "Ludlisto" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1617,374 +1223,310 @@ msgstr "Ludlisto aktualigintas" #: front/src/components/playlists/Form.vue:25 msgid "Playlist visibility" -msgstr "" +msgstr "Ludlistvideblo" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" -msgstr "" +msgstr "Ludlistoj" #: front/src/components/Home.vue:56 msgid "Playlists? We got them" -msgstr "" +msgstr "Ludlistoj? Jen" #: front/src/components/auth/Settings.vue:79 msgid "Please double-check your password is correct" -msgstr "" +msgstr "Bonvolu rekontroli ke via pasvorto ĝustas" #: front/src/components/auth/Login.vue:9 msgid "Please double-check your username/password couple is correct" -msgstr "" +msgstr "Bonvolu rekontroli ke vian uzantnomo kaj pasvorto ĝustas" #: front/src/components/auth/Settings.vue:46 msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." -msgstr "" +msgstr "PNG, GIF, aŭ JPG. Maksimume 2Mo. La bildo malgrandigos al 400×400 rastrumero." -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" -msgstr "" +#: front/src/components/library/FileUpload.vue:58 +msgid "Proceed" +msgstr "Konfirmi" #: front/src/views/auth/EmailConfirm.vue:26 #: front/src/views/auth/PasswordResetConfirm.vue:31 msgid "Proceed to login" -msgstr "" +msgstr "Ensalutu" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" -msgstr "" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" +msgstr "Procedas" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" -msgstr "" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "Purigi" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "Purigi erarajn dosierojn?" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "Purigi atendantajn dosierojn?" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" +msgstr "Purigi ignoratajn dosierojn?" #: front/src/components/Sidebar.vue:20 msgid "Queue" -msgstr "" - -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "" - -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "" - -#: front/src/components/library/radios/Builder.vue:226 -msgid "Radio Builder" -msgstr "" +msgstr "Atendovico" #: front/src/components/library/radios/Builder.vue:15 msgid "Radio created" -msgstr "" +msgstr "Kreis radion" #: front/src/components/library/radios/Builder.vue:21 msgid "Radio name" -msgstr "" +msgstr "Nomo de la radio" #: front/src/components/library/radios/Builder.vue:12 -#, fuzzy msgid "Radio updated" -msgstr "Kreodato" +msgstr "Ĝisdatigas radion" #: front/src/components/library/Library.vue:10 -#: src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 msgid "Radios" -msgstr "" +msgstr "Radioj" #: front/src/views/instance/Timeline.vue:7 msgid "Recent activity on this instance" -msgstr "" +msgstr "Nova agado en ĉi-tiu instanco" #: front/src/components/library/Home.vue:24 msgid "Recently added" -msgstr "" +msgstr "Novaj aldonoj" #: front/src/components/library/Home.vue:11 msgid "Recently favorited" -msgstr "" +msgstr "Novaj stelumoj" #: front/src/components/library/Home.vue:6 msgid "Recently listened" -msgstr "" +msgstr "Lastatempaj aŭskultantoj" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "" +msgstr "Ĝisdatigi" #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" -msgstr "" +msgstr "Aligis je %{date}" #: front/src/components/auth/Signup.vue:9 msgid "Registration are closed on this instance, you will need an invitation code to signup." -msgstr "" +msgstr "Registrigadoj fermitas je tiu instanco, vi bezonos invitkodon por registrigi." #: front/src/components/manage/users/UsersTable.vue:71 msgid "regular user" -msgstr "" +msgstr "normala uzanto" + +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "Malakcepti" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "Malakceptinta" + +#: front/src/views/content/remote/Home.vue:6 +msgid "Remote libraries" +msgstr "Mallokaj muzikejoj" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." +msgstr "Mallokaj muzikejoj apartenas al aliaj uzantoj el la reto. Vi povas atingi ilin se ili estas publika aŭ vi estas akceptinta." #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" -msgstr "" +msgstr "Forigi" #: front/src/components/auth/Settings.vue:58 msgid "Remove avatar" -msgstr "" - -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "" +msgstr "Forigi profilbildon" #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" -msgstr "" +msgstr "Demandi novan pasvorton" #: front/src/components/auth/SubsonicTokenForm.vue:35 msgid "Request a new Subsonic API password?" -msgstr "" +msgstr "Demandi novan Subsonic API pasvorton?" #: front/src/components/auth/SubsonicTokenForm.vue:43 msgid "Request a password" -msgstr "" - -#: front/src/App.vue:35 -msgid "Request music" -msgstr "" - -#: front/src/views/library/MusicRequest.vue:4 -#: src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "" +msgstr "Demandi pasvorton" #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "" +msgstr "Demando sendinta!" #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" -msgstr "" - -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "" +msgstr "Renuligadi vian pasvorton" #: front/src/components/favorites/List.vue:38 #: src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 -#: src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" -msgstr "" +msgstr "Rezultoj per paĝo" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:29 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" -msgstr "" +msgstr "Konservi" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" -msgstr "" +#: front/src/views/content/remote/Card.vue:31 +msgid "Scan pending" +msgstr "Atendanta skano" + +#: front/src/views/content/remote/Card.vue:43 +msgid "Scanned successfully" +msgstr "Sukcese skanis" + +#: front/src/views/content/remote/Card.vue:47 +msgid "Scanned with errors" +msgstr "Malsukcese skanis" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" +msgstr "Skanas… (%{progress}%)" -#: front/src/components/federation/LibraryTrackTable.vue:5 #: front/src/components/library/Artists.vue:10 #: src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:5 #: src/views/playlists/List.vue:13 msgid "Search" -msgstr "" +msgstr "Serĉi" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "" - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "" - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "" - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "" - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "" - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." -msgstr "" +#: front/src/views/content/remote/ScanForm.vue:9 +msgid "Search a remote library" +msgstr "Serĉi mallokan muzikejon" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" -msgstr "" +msgstr "Serĉi muzikon" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" -msgstr "" +msgstr "Serĉi je lyrics.wikia.com" #: front/src/components/library/Album.vue:33 #: src/components/library/Artist.vue:31 #: front/src/components/library/Track.vue:40 msgid "Search on Wikipedia" -msgstr "" - -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "" +msgstr "Serĉi je Vikipedio" #: front/src/views/admin/Settings.vue:15 msgid "Sections" -msgstr "" +msgstr "Sekcioj" -#: front/src/components/library/radios/Builder.vue:41 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" -msgstr "" +msgstr "Elekti filtrilon" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Elekti unu eron" +msgstr[1] "Elekti ĉiun la %{total} erojn" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "" - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" -msgstr "" +msgstr "Elekti nur la aktualan uzon" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "" - -#: front/src/components/Sidebar.vue:97 -#: src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" -msgstr "" +msgstr "Agordoj" #: front/src/components/auth/Settings.vue:10 msgid "Settings updated" -msgstr "" +msgstr "Agordoj ĝisdatigas" #: front/src/components/admin/SettingsGroup.vue:11 msgid "Settings updated successfully." -msgstr "" - -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "" +msgstr "Agordoj sukcese ĝisdatigas." #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" +msgstr "Diskonigi ligilon" + +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." msgstr "" +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +msgid "Sharing link" +msgstr "" + +#: front/src/components/audio/album/Card.vue:40 +#, fuzzy +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "%{count} kanto" +msgstr[1] "%{count} kantoj" + #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" msgstr[0] "" msgstr[1] "" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "" -msgstr[1] "" +#: front/src/views/Notifications.vue:10 +#, fuzzy +msgid "Show read notifications" +msgstr "Lasta redakto" -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" msgstr "" -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "" - #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" msgstr "" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "" - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 +#: src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 msgid "Skipped" msgstr "" +#: front/src/views/content/libraries/Quota.vue:49 +msgid "Skipped files" +msgstr "" + #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "" @@ -1997,21 +1539,15 @@ msgstr "" msgid "Sorry, we did not found any artist matching your query" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" msgstr "" @@ -2020,26 +1556,11 @@ msgstr "" msgid "Start" msgstr "" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "" @@ -2047,15 +1568,11 @@ msgstr "" msgid "Stop" msgstr "" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" msgstr "" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" msgstr "" @@ -2063,46 +1580,43 @@ msgstr "" msgid "Submit another request" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "" - #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" msgstr "" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" +#: front/src/App.vue:13 +msgid "Suggested choices" msgstr "" -#: front/src/App.vue:11 -msgid "Suggested choices" +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" msgstr "" #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "" +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." msgstr "" -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "" - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." msgstr "" -#: front/src/components/Home.vue:124 +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." +msgstr "" + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "" + +#: front/src/components/Home.vue:121 msgid "The plaform is free and open-source, you can install it and modify it without worries" msgstr "" @@ -2110,32 +1624,36 @@ msgstr "" msgid "The Subsonic API is not available on this Funkwhale instance." msgstr "" -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" msgstr "" -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" msgstr "" -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." msgstr "" #: front/src/components/auth/Profile.vue:16 msgid "This is you!" msgstr "" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." msgstr "" -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "" + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" msgstr "" #: front/src/views/playlists/Detail.vue:37 @@ -2150,9 +1668,163 @@ msgstr "" msgid "This will completely disable access to the Subsonic API using from account." msgstr "" -#: front/src/App.vue:137 +#: front/src/App.vue:162 src/components/About.vue:55 +#: src/components/Home.vue:154 front/src/components/PageNotFound.vue:33 +#: src/components/Sidebar.vue:203 front/src/components/Sidebar.vue:204 +#: src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 +#: src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 +#: src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 +#: src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 +#: src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 +#: src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 +#: src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 +#: src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 +#: src/components/audio/SearchBar.vue:27 front/src/components/auth/Login.vue:77 +#: src/components/auth/Login.vue:78 front/src/components/auth/Logout.vue:20 +#: src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 +#: src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 +#: src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 +#: src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 +#: src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 +#: src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 +#: src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 +#: src/views/auth/EmailConfirm.vue:51 front/src/views/auth/PasswordReset.vue:52 +#: src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +#: src/views/content/Base.vue:19 front/src/views/content/Home.vue:35 +#: src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 +#: src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 +#: src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 +#: src/views/instance/Timeline.vue:57 front/src/views/playlists/Detail.vue:90 +#: src/views/playlists/List.vue:104 front/src/views/playlists/List.vue:105 +#: src/views/radios/Detail.vue:80 msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "" +msgstr[1] "" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." @@ -2162,17 +1834,23 @@ msgstr "" msgid "This will remove all tracks from this playlist and cannot be undone." msgstr "" -#: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 -#: front/src/components/manage/library/FilesTable.vue:37 -msgid "Title" +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." msgstr "" -#: front/src/components/audio/SearchBar.vue:27 -#: src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/components/audio/track/Table.vue:6 +#: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 +msgid "Title" msgstr "" #: front/src/components/library/Track.vue:53 @@ -2189,15 +1867,11 @@ msgstr "" #: front/src/components/library/Album.vue:43 #: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 -#: front/src/views/radios/Detail.vue:34 +#: src/views/content/Base.vue:8 front/src/views/content/libraries/Detail.vue:8 +#: src/views/playlists/Detail.vue:50 front/src/views/radios/Detail.vue:34 msgid "Tracks" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "" - #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" msgstr "" @@ -2210,34 +1884,39 @@ msgstr "" msgid "tracks listened" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "" - +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" msgstr "" +#: front/src/views/content/remote/Card.vue:100 +#: src/views/content/remote/Card.vue:105 +#, fuzzy +msgid "Unfollow" +msgstr "Sekvi" + +#: front/src/views/content/remote/Card.vue:101 +#, fuzzy +msgid "Unfollow this library?" +msgstr "Iru al la muzikejo" + #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." msgstr "" -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "" - #: front/src/components/Home.vue:37 msgid "Unlimited music" msgstr "" -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "" - #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" msgstr "" +#: front/src/views/content/libraries/Form.vue:25 +#, fuzzy +msgid "Update library" +msgstr "Manipuli muzikejon" + #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" msgstr "" @@ -2250,8 +1929,10 @@ msgstr "" msgid "Update your password" msgstr "" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" msgstr "" @@ -2259,39 +1940,54 @@ msgstr "" msgid "Upload a new avatar" msgstr "" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" +#: front/src/views/content/Home.vue:6 +msgid "Upload audio content" msgstr "" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/views/content/libraries/FilesTable.vue:54 +#, fuzzy +msgid "Upload date" +msgstr "Importdato" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:31 +#, fuzzy +msgid "Upload new tracks" +msgstr "Elŝuti kantojn" + +#: front/src/views/admin/users/UsersDetail.vue:82 +#, fuzzy +msgid "Upload quota" +msgstr "Importdato" + +#: front/src/components/library/FileUpload.vue:99 +msgid "Uploaded" +msgstr "" + +#: front/src/components/library/FileUpload.vue:5 +msgid "Uploading" +msgstr "" + +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "" -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" msgstr "" -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "" - #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." msgstr "" -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "" - #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" msgstr "" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" msgstr "" @@ -2299,12 +1995,20 @@ msgstr "" msgid "User activity" msgstr "" +#: front/src/components/library/Album.vue:49 +#: src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +#, fuzzy +msgid "User libraries" +msgstr "Muzikejoj" + #: front/src/components/library/Radios.vue:20 msgid "User radios" msgstr "" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" msgstr "" @@ -2316,21 +2020,32 @@ msgstr "" msgid "users" msgstr "" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 +#: src/views/admin/users/UsersList.vue:21 front/src/views/admin/Settings.vue:81 msgid "Users" msgstr "" +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +#, fuzzy +msgid "View files" +msgstr "Muzikejaj dosieroj" + #: front/src/components/library/Album.vue:37 #: src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" msgstr "" +#: front/src/views/content/libraries/Form.vue:18 +#, fuzzy +msgid "Visibility" +msgstr "Ludlistvideblo" + #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" msgstr "" @@ -2355,12 +2070,20 @@ msgstr "" msgid "We cannot save your settings" msgstr "" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" msgstr "" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:40 +msgid "we recommend using Picard for that purpose" msgstr "" #: front/src/components/Home.vue:7 @@ -2375,27 +2098,15 @@ msgstr "" msgid "We've received your request, you'll get some groove soon ;)" msgstr "" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" msgstr "" -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "" - #: front/src/components/Home.vue:24 msgid "Why funkwhale?" msgstr "" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" msgstr "" @@ -2403,15 +2114,19 @@ msgstr "" msgid "Yes, log me out!" msgstr "" +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" +msgstr "" + #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" msgstr "" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." msgstr "" -#: front/src/components/Home.vue:136 +#: front/src/components/Home.vue:133 msgid "You can invite friends and family to your instance so they can enjoy your music" msgstr "" @@ -2423,14 +2138,18 @@ msgstr "" msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." msgstr "" -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" msgstr "" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" msgstr "" +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." +msgstr "" + #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" msgstr "" @@ -2439,22 +2158,19 @@ msgstr "" msgid "You will have to update your password on your clients that use this password." msgstr "" -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "" - #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." msgstr "" -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" -msgstr "" - -#: front/src/components/Home.vue:117 +#: front/src/components/Home.vue:114 msgid "Your music, your way" msgstr "" +#: front/src/views/Notifications.vue:7 +#, fuzzy +msgid "Your notifications" +msgstr "Lasta redakto" + #: front/src/views/auth/PasswordResetConfirm.vue:29 msgid "Your password has been updated successfully." msgstr "" @@ -2463,8 +2179,423 @@ msgstr "" msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" msgstr "" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +#, fuzzy +msgid "Activity visibility" +msgstr "Ludlistvideblo" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 +#: src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "Neniu krom mi" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 +#: src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "Ĉiu en ĉi-tiu instanco" + +#: front/src/components/mixins/Translations.vue:18 +#, fuzzy +msgid "Accessed date" +msgstr "Atingo malaktivigas" + +#: front/src/components/mixins/Translations.vue:19 +#, fuzzy +msgid "Modification date" +msgstr "Fortempiĝa dato" + +#: front/src/components/mixins/Translations.vue:20 +#, fuzzy +msgid "Imported date" +msgstr "Importdato" + +#: front/src/components/mixins/Translations.vue:22 +#, fuzzy +msgid "Track name" +msgstr "Nomo de la radio" + +#: front/src/components/mixins/Translations.vue:23 +#, fuzzy +msgid "Album name" +msgstr "Albumpagô" + +#: front/src/components/mixins/Translations.vue:30 +#, fuzzy +msgid "Sign-up date" +msgstr "Agordoj ĝisdatigas" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "Kopii kantojn el la aktuala atendovico en tiu ludlisto" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "Aldoni al tiu ludlisto" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "Mia mojosa ludlisto" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "Ĉiu" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "Tajpu vian invitkodon (usklecoblindan)" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "Tajpu vian uzantnomon" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "Tajpu vian retadreson" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "Pasvorto aktuliginta" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "Atingo malaktivigas" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "Tajpu vian uzantnomon aŭ retadreson" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "Ensaluti" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "Profilo de %{username}" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "Elsaluti" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "Preferoj de via konto" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "Tajpu nomon de radio…" + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "Faranto de radio" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "Mia mojosa radio" + +#: front/src/components/library/radios/Builder.vue:236 +#, fuzzy +msgid "My awesome description" +msgstr "Mia mojosa radio" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "" + +#: front/src/components/library/FileUpload.vue:240 +#, fuzzy +msgid "A network error occured while uploading this file" +msgstr "Eraro okazis kiam konservi viajn ŝanĝojn" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "Tajpu nomon de artisto…" + +#: front/src/components/library/Track.vue:195 +#: src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "Hejmo" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "" + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" msgstr[0] "" msgstr[1] "" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "Artisto, albumo, kanto…" + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "" + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "Atendovico miksiĝis!" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "Antaŭa kanto" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "Ludi kanton" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "Paŭzi kanton" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "Baldaŭa kanto" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "Silentigi" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "Ripeto malaktivas. Alklaki por aktivi ripetado de la aktuala kanto." + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "Ripetas unu kanton. Alklaki por aktivi ripetado de la tutan atendovico." + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "Ripetas la tutan atendovicon. Alklaki por malaktivi ripeto." + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "Purigi vian atendovico" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "Atendantaj importpetoj" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "Atendantaj petoj da sekvado" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "Tajpu vian serĉon…" + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "" + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "Lasu malplena por hazarda kodo" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "" + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "" + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "Federo" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "" + +#: front/src/views/content/remote/ScanForm.vue:48 +#, fuzzy +msgid "Enter a library url" +msgstr "Tajpu domajna nomo de instance…" + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:226 +msgid "Search by title, artist, album..." +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:242 +#, fuzzy +msgid "Import went on successfully" +msgstr "Pasvorto sukcese aktualiginta" + +#: front/src/views/content/libraries/FilesTable.vue:259 +#, fuzzy +msgid "Relaunch import" +msgstr "Fini importadon" + +#: front/src/views/content/libraries/Card.vue:58 +#, fuzzy +msgid "Visibility: nobody except me" +msgstr "Neniu krom mi" + +#: front/src/views/content/libraries/Card.vue:59 +#, fuzzy +msgid "Visibility: everyone on this instance" +msgstr "Ĉiu en ĉi-tiu instanco" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:61 +msgid "Total size of the files in this library" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:70 +#, fuzzy +msgid "My awesome library" +msgstr "Mia mojosa radio" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:74 +#, fuzzy +msgid "Everyone, including other instances" +msgstr "Ĉiu en ĉi-tiu instanco" + +#: front/src/views/content/libraries/Form.vue:106 +#, fuzzy +msgid "Library updated" +msgstr "Nomo de muzikejo" + +#: front/src/views/content/libraries/Form.vue:109 +#, fuzzy +msgid "Library created" +msgstr "Nomo de muzikejo" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "Radio" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "Instanca tempolino" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "Ludlisto" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "Tajpu ludlistan nomon…" + +#: front/src/views/admin/library/Base.vue:16 +#, fuzzy +msgid "Manage library" +msgstr "En muzikejo" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "Elekti ĉu la konton aktivas aŭ ne. Malaktivaj konton ne eblas ensaluti aŭ uzi la servico." + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "" + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "Manipuli uzantojn" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "Instancaj preferoj" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "Instanca informo" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "Eraroraportado" diff --git a/front/locales/es/LC_MESSAGES/app.po b/front/locales/es/LC_MESSAGES/app.po index 8608d2074..6027e0405 100644 --- a/front/locales/es/LC_MESSAGES/app.po +++ b/front/locales/es/LC_MESSAGES/app.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: front 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-22 14:12+0200\n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" "PO-Revision-Date: 2018-07-24 19:50+0000\n" "Last-Translator: anonymous \n" "Language-Team: none\n" @@ -30,14 +30,16 @@ msgstr "(%{ index } de %{ length })" msgid "(empty)" msgstr "(vacío)" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "%{ count } de %{ total } elemento seleccionado" msgstr[1] "%{ count } de %{ total } elementos seleccionados" -#: front/src/components/Sidebar.vue:116 src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 +#: src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" msgstr[0] "%{ count } canción" @@ -49,7 +51,7 @@ msgid_plural "%{ count } tracks in %{ albumsCount } albums" msgstr[0] "%{ count } canción en %{ albumsCount } álbumes" msgstr[1] "%{ count } canciones en %{ albumsCount } álbumes" -#: front/src/components/library/radios/Builder.vue:76 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" msgstr[0] "%{ count } canción coincidiendo con filtros combinados" @@ -61,6 +63,11 @@ msgid_plural "%{ count } tracks" msgstr[0] "%{ count} canción" msgstr[1] "%{ count } canciones" +#: front/src/views/content/libraries/Quota.vue:11 +#, fuzzy +msgid "%{ current } used on %{ max } allowed" +msgstr "%{ count } de %{ total } elemento seleccionado" + #: front/src/components/common/Duration.vue:2 msgid "%{ hours } h %{ minutes } min" msgstr "%{ hours } h %{ minutes } min" @@ -77,10 +84,6 @@ msgstr "%{ user } añadió una canción a sus favoritos" msgid "%{ user } listened to a track" msgstr "%{ user } escuchó una canción" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "Perfil de %{ username }" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" @@ -93,39 +96,30 @@ msgid_plural "%{ count } favorites" msgstr[0] "1 favorito" msgstr[1] "%{ count } favoritos" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "1 canción" -msgstr[1] "%{ count } canciones" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "Sobre %{ instance }" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "Acerca de Funkwhale" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "Acerca de esta instancia" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 +#: front/src/views/content/libraries/Detail.vue:48 +#, fuzzy +msgid "Accept" +msgstr "Aceptado" + +#: front/src/views/content/libraries/Detail.vue:40 msgid "Accepted" msgstr "Aceptado" -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "Acceso deshabilitado" - -#: front/src/components/Home.vue:109 +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" -msgstr "" -"Accede a tu música con una interfaz limpia y concéntrate en lo que realmente " -"importa" +msgstr "Accede a tu música con una interfaz limpia y concéntrate en lo que realmente importa" #: front/src/views/admin/users/UsersDetail.vue:54 msgid "Account active" @@ -135,10 +129,6 @@ msgstr "Cuenta activa" msgid "Account settings" msgstr "Configuración de cuenta" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "Configuración de Cuenta" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "Estado de cuenta" @@ -147,16 +137,19 @@ msgstr "Estado de cuenta" msgid "Account's email" msgstr "Correo electrónico de la cuenta" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +#, fuzzy +msgid "Action" +msgstr "Acciones" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" msgstr[0] "Acción %{ action } fue iniciado exitosamente en %{ count } elemento" msgstr[1] "Acción %{ action } fue iniciado exitosamente en %{ count } elementos" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:60 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "Acciones" @@ -164,30 +157,22 @@ msgstr "Acciones" msgid "Active" msgstr "Activo" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "Actividad" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" -msgstr "Actor" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" +msgstr "" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "Añadir una nueva biblioteca" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" msgstr "Añadir filtro" -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" msgstr "Añade filtros para personalizar tu radio" -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "Añadir a cola de reproducción actual" - #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" @@ -202,10 +187,6 @@ msgstr "Añadir a lista de reproducción..." msgid "Add to queue" msgstr "Añadir a la cola de reproducción" -#: front/src/components/playlists/PlaylistModal.vue:116 -msgid "Add to this playlist" -msgstr "Añadir a esta lista de reproducción" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "Añadir canción" @@ -214,24 +195,19 @@ msgstr "Añadir canción" msgid "Admin" msgstr "Admin" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" msgstr "Administración" -#: front/src/components/audio/SearchBar.vue:26 src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 +#: src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "Álbum" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "Álbum %{ title } (%{ count } track) de %{ artist }" -msgstr[1] "Álbum %{ title } (%{ count } tracks) de %{ artist }" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" @@ -242,8 +218,8 @@ msgstr[1] "Álbum que contiene %{ count } canciones, de %{ artist }" msgid "Album page" msgstr "Página del álbum" -#: front/src/components/audio/Search.vue:19 src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 +#: front/src/components/audio/Search.vue:19 +#: src/components/instance/Stats.vue:48 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "Álbumes" @@ -252,8 +228,8 @@ msgstr "Álbumes" msgid "Albums by this artist" msgstr "Álbumes de este artista" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "Todo" @@ -263,71 +239,45 @@ msgstr "Ha ocurrido un error al guardar los cambios" #: front/src/components/auth/Login.vue:10 msgid "An unknown error happend, this can mean the server is down or cannot be reached" -msgstr "" -"Ha ocurrido un error desconocido, esto puede significar que el servidor está " -"fuera de servicio o no se puede conectar" - -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "Cualquiera" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "API" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "Aprobar" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "¿Aprobar el acceso?" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "Aprobado" +msgstr "Ha ocurrido un error desconocido, esto puede significar que el servidor está fuera de servicio o no se puede conectar" #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "" -#: front/src/components/audio/SearchBar.vue:25 src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 +#: src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" msgstr "Artista" #: front/src/components/requests/Form.vue:5 +#: src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "Nombre del artista" -#: front/src/components/library/Album.vue:22 src/components/library/Track.vue:23 +#: front/src/components/library/Album.vue:22 +#: src/components/library/Track.vue:23 msgid "Artist page" msgstr "Página del artista" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "Artista, álbum, canción..." - -#: front/src/components/audio/Search.vue:10 src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 src/components/library/Library.vue:7 +#: front/src/components/audio/Search.vue:10 +#: src/components/instance/Stats.vue:42 +#: front/src/components/library/Library.vue:7 +#: src/components/library/Artists.vue:120 msgid "Artists" msgstr "Artistas" #: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 src/components/library/Radios.vue:44 +#: src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" msgstr "Ascendente" @@ -335,10 +285,6 @@ msgstr "Ascendente" msgid "Ask for a password reset" msgstr "Restablecer contraseña" -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "Autoimportación" - #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" msgstr "Listas de reproducción disponibles" @@ -352,14 +298,9 @@ msgstr "Avatar" msgid "Back to login" msgstr "Volver a la página de conección" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "" -"Ten cuidado al aceptar solicitudes de seguimiento, porque significa que tu " -"seguidorx podrá acceder a tu biblioteca completa." - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "Bitrate" @@ -367,7 +308,7 @@ msgstr "Bitrate" msgid "Browse" msgstr "Explorar" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "Explorar biblioteca" @@ -375,18 +316,6 @@ msgstr "Explorar biblioteca" msgid "Browsing artists" msgstr "Explorando artistas" -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "Explorando canciones federadas" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "Explorando seguidorxs" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "Explorando bibliotecas" - #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" msgstr "Explorando listas de reproducción" @@ -403,22 +332,17 @@ msgstr "Editor" msgid "By %{ artist }" msgstr "De %{ artist }" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." -msgstr "Si confirmas, %{ username } no podrá acceder a tu biblioteca." +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." +msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "Si confirmas, %{ username } podrá acceder a tu biblioteca." - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" msgstr "Cancelar" -#: front/src/components/library/radios/Builder.vue:59 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "" @@ -426,7 +350,7 @@ msgstr "" msgid "Cannot change your password" msgstr "No se puede cambiar la contraseña" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" msgstr "Cambiar idioma" @@ -453,15 +377,13 @@ msgstr "Cambios sincronizados con el servidor" #: front/src/components/auth/Settings.vue:70 msgid "Changing your password will also change your Subsonic API password if you have requested one." -msgstr "" -"Cambiar tu contraseña también cambiará tu contraseña Subsonic API si pediste " -"una." +msgstr "Cambiar tu contraseña también cambiará tu contraseña Subsonic API si pediste una." #: front/src/components/auth/Settings.vue:98 msgid "Changing your password will have the following consequences" msgstr "Cambiar tu contraseña tendrá las siguientes consecuencias" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "Escoge tu instancia" @@ -478,22 +400,12 @@ msgstr "" msgid "Clear playlist" msgstr "Vaciar lista de reproducción" -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "Vaciar cola de reproducción" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "CLI" - #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" -msgstr "" -"Pulsa una sola vez y escucha durante horas, gracias a las radios integradas" +msgstr "Pulsa una sola vez y escucha durante horas, gracias a las radios integradas" -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" msgstr "" #: front/src/components/manage/users/InvitationForm.vue:26 @@ -507,12 +419,11 @@ msgstr "Código" msgid "Collapse" msgstr "Reducir" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "" -#: front/src/components/library/radios/Builder.vue:58 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" msgstr "Configurar" @@ -529,11 +440,11 @@ msgstr "Confirma tu correo electrónico" msgid "Confirmation code" msgstr "Código de confirmación" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" -msgstr "Copiar canciones de cola de reproducción actual a lista de reproducción" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" +msgstr "" -#: front/src/components/Home.vue:88 +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" msgstr "Covers, letras, queremos conseguirlo todo ;)" @@ -541,19 +452,28 @@ msgstr "Covers, letras, queremos conseguirlo todo ;)" msgid "Create a funkwhale account" msgstr "Crear una cuenta de funkwhale" +#: front/src/views/content/libraries/Home.vue:14 +#, fuzzy +msgid "Create a new library" +msgstr "Crear una nueva lista de reproducción" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "Crear una nueva lista de reproducción" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "Crear una cuenta" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "Crear importación" +#: front/src/views/content/libraries/Form.vue:26 +#, fuzzy +msgid "Create library" +msgstr "Una biblioteca impecable" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "Crear mi cuenta" @@ -566,9 +486,8 @@ msgstr "Crear una lista de reproducción" msgid "Create your own radio" msgstr "Crear tu propia radio" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "Fecha de creación" @@ -576,17 +495,37 @@ msgstr "Fecha de creación" msgid "Current avatar" msgstr "Avatar actual" +#: front/src/views/content/libraries/DetailArea.vue:4 +#, fuzzy +msgid "Current library" +msgstr "Una biblioteca impecable" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "Canción actual" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +#, fuzzy +msgid "Current usage" +msgstr "Canción actual" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:29 +#: src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "Borrar" +#: front/src/views/content/libraries/Form.vue:39 +#, fuzzy +msgid "Delete library" +msgstr "Una biblioteca impecable" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "Borrar lista de reproducción" @@ -595,34 +534,35 @@ msgstr "Borrar lista de reproducción" msgid "Delete radio" msgstr "Borrar radio" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" -msgstr "Rechazar" - -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "¿Rechazar el acceso?" +#: front/src/views/content/libraries/Form.vue:31 +#, fuzzy +msgid "Delete this library?" +msgstr "Llévame a la biblioteca" #: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 src/components/library/Radios.vue:47 +#: src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "Descendente" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +#, fuzzy +msgid "Description" +msgstr "Duración" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "Detalle" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." -msgstr "" -"Determina si la cuenta de usuario es activa o no. Los usuarios inactivos no " -"pueden conectarse ni usar el servicio." +#: front/src/views/content/remote/Card.vue:50 +#, fuzzy +msgid "Details" +msgstr "Detalle" #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 @@ -641,7 +581,7 @@ msgstr "¿Desactivar el acceso de API Subsonic?" msgid "Discover how to use Funkwhale from other apps" msgstr "Descubrir cómo utilizar Funkwhale desde otras aplicaciones" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "Mostrar públicamente" @@ -667,42 +607,44 @@ msgid_plural "Do you want to launch %{ action } on %{ count } elements?" msgstr[0] "¿Quieres realizar la acción %{ action } en %{ count } elemento?" msgstr[1] "¿Quieres realizar la acción %{ action } en %{ count } elementos?" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "¿Quieres restaurar tu cola de reproducción anterior?" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "Documentación" -#: front/src/components/audio/track/Table.vue:24 src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "Descargar" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "Descargar canciones" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" -msgstr "" -"Arrastra y suelta las filas para reordenar canciones en la lista de " -"reproducción" +msgstr "Arrastra y suelta las filas para reordenar canciones en la lista de reproducción" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 +#: src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "Duración" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "Fácil de usar" +#: front/src/views/content/libraries/Detail.vue:9 +#, fuzzy +msgid "Edit" +msgstr "Editar..." + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "Editar la información de esta instancia" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "Editar..." @@ -723,58 +665,17 @@ msgstr "Correo electrónico confirmado" msgid "End edition" msgstr "Terminar la edición" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "" -"Asegúrate de que tus archivos de música estén bien etiquetados antes de " -"subirlos." - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "Introducir un nombre de radio..." - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "Introducir un nombre de artista..." - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "Introducir un nombre de dominio de biblioteca..." - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "Introducir un nombre de lista de reproducción..." - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "Introducir tu correo electrónico" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "" -"Introducir tu código de invitación (no distingue mayúsculas de minúsculas)" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "Introducir tu búsqueda..." - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "Introducir tu nombre de usuario" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "Introducir tu nombre de usuario o correo electrónico" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "Error" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" +#: front/src/views/content/remote/Card.vue:39 +#, fuzzy +msgid "Error during scan" msgstr "Informes de error" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "Error al aplicar la acción" @@ -794,29 +695,31 @@ msgstr "Error al confirmar tu correo electrónico" msgid "Error while creating invitation" msgstr "Error al crear la invitación" +#: front/src/views/content/remote/ScanForm.vue:3 +#, fuzzy +msgid "Error while fetching remote library" +msgstr "Error al escanear la biblioteca" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "Error al guardar los cambios" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "Error al escanear la biblioteca" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "En error" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" -msgstr "Todo el mundo" +#: front/src/views/content/libraries/Quota.vue:75 +#, fuzzy +msgid "Errored files" +msgstr "En error" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" -msgstr "Todo el mundo en esta instancia" +#: front/src/views/content/remote/Card.vue:58 +#, fuzzy +msgid "Errored tracks:" +msgstr "Canciones federadas" -#: front/src/components/library/radios/Builder.vue:57 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "Excluir" @@ -825,6 +728,7 @@ msgid "Expand" msgstr "Expandir" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "Fecha de caducidad" @@ -836,92 +740,51 @@ msgstr "Caducada" msgid "Expired/used" msgstr "Caducada/usada" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "Fuente externa. Back-ends soportados" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "Favoritos" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "Federar con una nueva instancia" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "Canciones federadas" - -#: front/src/components/Sidebar.vue:87 src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "Federación" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "Replicación de archivos" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "Nombre del archivo" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "Subida de archivo" - -#: front/src/views/admin/library/Base.vue:5 src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/library/Base.vue:5 +#: src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "Archivos" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "Filtrar tipos de álbum" - -#: front/src/components/library/radios/Builder.vue:56 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "Nombre del filtro" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "Terminar importación" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "Terminado" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "Para empezar, elige la fuente de importación de la música" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "Seguir" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" +#: front/src/views/content/remote/Card.vue:88 +#, fuzzy +msgid "Follow pending approval" msgstr "Solicitud de seguimiento pendiente de aprobación" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" -msgstr "Estado de seguimiento" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" +msgstr "" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "Seguidores" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "Solo seguidorxs" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "Siguiendo" -#: front/src/components/activity/Like.vue:12 src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 +#: src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "del álbum %{ album } de %{ artist }" @@ -929,30 +792,23 @@ msgstr "del álbum %{ album } de %{ artist }" msgid "From album %{ album } by %{ artist }" msgstr "Del álbum %{ album } de %{ artist }" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" -msgstr "" -"Funkwhale es un proyecto gratuito de código fuente libre, gestionado por " -"voluntarixs. Puedes ayudarnos a mejorar el servicio informando de errores, " -"sugiriendo mejoras y compartiendo el proyecto con tus amigxs!" +msgstr "Funkwhale es un proyecto gratuito de código fuente libre, gestionado por voluntarixs. Puedes ayudarnos a mejorar el servicio informando de errores, sugiriendo mejoras y compartiendo el proyecto con tus amigxs!" #: front/src/components/auth/SubsonicTokenForm.vue:7 msgid "Funkwhale is compatible with other music players that support the Subsonic API." -msgstr "" -"Funkwhale es compatible con otros reproductores de música que soportan la " -"API Subsonic." +msgstr "Funkwhale es compatible con otros reproductores de música que soportan la API Subsonic." -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "Funkwhale es facilísimo de usar." #: front/src/components/Home.vue:39 msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." -msgstr "" -"Funkwhale está diseñado para que sea fácil escuchar la música que te gusta, " -"o descubrir nuevos artistas." +msgstr "Funkwhale está diseñado para que sea fácil escuchar la música que te gusta, o descubrir nuevos artistas." -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." msgstr "Funkwhale es gratis y te da el control de tu música." @@ -968,14 +824,15 @@ msgstr "Obtener una nueva invitación" msgid "Get me to the library" msgstr "Llévame a la biblioteca" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " +#: front/src/components/Home.vue:76 +msgid "Get quality metadata about your music thanks to MusicBrainz" msgstr "" +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +#, fuzzy +msgid "Get started" +msgstr "Próximo paso" + #: front/src/components/common/ActionTable.vue:21 #: front/src/components/common/ActionTable.vue:27 msgid "Go" @@ -985,175 +842,58 @@ msgstr "Ir" msgid "Go to home page" msgstr "Ir a la página principal" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "Captar metadatos correspondientes" - -#: front/src/App.vue:74 +#: front/src/App.vue:73 msgid "Help us translate Funkwhale" msgstr "Ayúdanos a traducir Funkwhale" -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "Inicio" - #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" msgstr "Horas de música" #: front/src/components/auth/SubsonicTokenForm.vue:11 msgid "However, accessing Funkwhale from those clients require a separate password you can set below." -msgstr "" -"Sin embargo, acceder a Funkwhale desde estos clientes requiere una " -"contraseña distinta que podrás configurar a continuación." - -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "ID" +msgstr "Sin embargo, acceder a Funkwhale desde estos clientes requiere una contraseña distinta que podrás configurar a continuación." #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." -msgstr "" -"Si la dirección de correo electrónico proporcionada en el paso anterior es " -"válida y asociada a una cuenta de usuarix, deberías recibir un correo " -"electrónico con las instrucciones de restablecimiento dentro de unos minutos." - -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "Importación" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "Importación #%{ id } iniciada" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "Importar %{ count } canción" -msgstr[1] "Importar %{ count } canciones" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "Lote de importación" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "Lote de importación #%{ id }" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "Lotes de importación" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "Lotes de importación" +msgstr "Si la dirección de correo electrónico proporcionada en el paso anterior es válida y asociada a una cuenta de usuarix, deberías recibir un correo electrónico con las instrucciones de restablecimiento dentro de unos minutos." #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "Fecha de importación" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "Página de detalles de importación" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "Importar música" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "Importar música" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" msgstr "Importa música desde otras plataformas, como YouTube o SoundCloud" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" -msgstr "Importación pendiente" - -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "Solicitudes de importación" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" +#: front/src/components/library/FileUpload.vue:51 +#, fuzzy +msgid "Import reference" msgstr "Fuente de la importación" -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "Estado de la importación" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "Importar este álbum" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "Importar esta canción" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "Importado" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "URL importada" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "Importaciones" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "En favoritos" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "En biblioteca" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "Inactivo" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "Introducir ID MusicBrainz de forma manual:" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "Introducir la dirección de correo electrónico asociada a tu cuenta" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" msgstr[0] "" msgstr[1] "" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "Información de esta instancia" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "Radios de la instancia" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "Ajustes de la instancia" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "Linea de tiempo de la instancia" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1163,55 +903,49 @@ msgstr "Código de invitación" msgid "Invitation code (optional)" msgstr "Código de invitación (opcional)" -#: front/src/views/admin/users/Base.vue:8 src/views/admin/users/InvitationsList.vue:3 +#: front/src/views/admin/users/Base.vue:8 +#: src/views/admin/users/InvitationsList.vue:3 #: front/src/views/admin/users/InvitationsList.vue:24 msgid "Invitations" msgstr "Invitaciones" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "Sistema de seguimiento de incidentes" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "ID de la tarea" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" -msgstr "Tareas" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" +msgstr "" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" msgstr "Apunta tus canciones favoritas" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." -msgstr "" -"Mantén tu PRIVATE_TOKEN en secreto ya que permite el acceso a tu cuenta." +#: front/src/views/content/remote/Home.vue:14 +#, fuzzy +msgid "Known libraries" +msgstr "Explorando bibliotecas" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "Última actividad" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "Última recuperación" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "Última modificación" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +#, fuzzy +msgid "Last update:" +msgstr "Lista de reproducción actualizada" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "Iniciar" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "Fecha de inicio" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" msgstr "Iniciar escaneado" @@ -1219,24 +953,21 @@ msgstr "Iniciar escaneado" msgid "Learn more about this instance" msgstr "Aprender más acerca de esta instancia" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "Dejar en blanco para un código aleatorio" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "Deja este campo en blanco si estás pidiendo la discografía completa." -#: front/src/views/federation/Base.vue:5 src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "Bibliotecas" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "" + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "Biblioteca" @@ -1244,161 +975,119 @@ msgstr "Biblioteca" msgid "Library files" msgstr "Archivos de biblioteca" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "Nombre de biblioteca" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "Tamaño de biblioteca" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "library@demo.funkwhale.audio" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "Enlaces" +#: front/src/views/content/libraries/Detail.vue:21 +#, fuzzy +msgid "Loading followers..." +msgstr "Explorando seguidorxs" + +#: front/src/views/content/libraries/Home.vue:3 +#, fuzzy +msgid "Loading Libraries..." +msgstr "Cargando tus favoritos…" + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +#, fuzzy +msgid "Loading library data..." +msgstr "Introducir un nombre de dominio de biblioteca..." + +#: front/src/views/Notifications.vue:4 +#, fuzzy +msgid "Loading notifications..." +msgstr "Cargando linea de tiempo…" + +#: front/src/views/content/remote/Home.vue:3 +#, fuzzy +msgid "Loading remote libraries..." +msgstr "Cargando linea de tiempo…" + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "Cargando linea de tiempo…" +#: front/src/views/content/libraries/Quota.vue:4 +#, fuzzy +msgid "Loading usage data..." +msgstr "Cargando tus favoritos…" + #: front/src/components/favorites/List.vue:5 msgid "Loading your favorites..." msgstr "Cargando tus favoritos…" -#: front/src/components/auth/Login.vue:78 -msgid "Log In" -msgstr "Iniciar sesión" - #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "Iniciar sesión con tu cuenta de Funkwhale" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "Cerrar sesión" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "Sesión iniciada como %{ username }" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "Iniciar sesión" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "Cerrar sesión" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "" -"Bucle deshabilitado. Pulsa para cambiar a reproducción en bucle de la " -"canción actual." - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "" -"Bucle de la canción actual. Pulsa para cambiar a la reproducción en bucle de " -"la cola de reproducción entera." - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "" -"Bucle de la cola de reproducción entera. Pulsa para desactivar la " -"reproducción en bucle." - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "Letras" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "Gestionar biblioteca" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "Gestionar listas de reproducción" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "Gestionar usuarixs" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "Gestionar tus listas de reproducción" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" -msgstr "Marcar como cerrado" - -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" +#: front/src/views/Notifications.vue:17 +#, fuzzy +msgid "Mark all as read" msgstr "Marcar como importado" -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "Metadatos" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" msgstr "" -"Los metadatos son los datos relacionados a la música que quieres importar. " -"Incluyen toda la información de los artistas, álbumes y canciones. Para " -"conseguir una biblioteca de calidad, es recomendado usar los metadatos del " -"proyecto \n" -" \n" -" MusicBrainz\n" -" \n" -" , una base de datos parecida a Wikipedia, pero para la música." -#: front/src/components/Sidebar.vue:48 src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "Música" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "Solicitud de música" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "Silencio" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "Mi cuenta" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" -msgstr "Mi lista de reproducción molona" +#: front/src/views/content/libraries/Home.vue:6 +#, fuzzy +msgid "My libraries" +msgstr "Bibliotecas" -#: front/src/components/library/radios/Builder.vue:227 -msgid "My awesome radio" -msgstr "Mi radio molona" - -#: front/src/components/library/Track.vue:64 src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 +#: src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 +#: src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "N/A" #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "Nombre" @@ -1407,48 +1096,41 @@ msgstr "Nombre" msgid "New password" msgstr "Nueva contraseña" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "Las nuevas canciones se agregarán aquí de forma automática." -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "Próximo paso" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "Próxima canción" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "No" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" msgstr "Sin complementos, sin extensiones : basta con una biblioteca en la web" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "No hay letra disponible para esta canción." -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" -msgstr "Solo yo" +#: front/src/components/federation/LibraryWidget.vue:6 +#, fuzzy +msgid "No matching library." +msgstr "Introducir un nombre de dominio de biblioteca..." -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "No siguiendo" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" -msgstr "No importado" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" +msgstr "" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "No usado" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +#, fuzzy +msgid "Notifications" +msgstr "Última modificación" + +#: front/src/App.vue:36 msgid "Official website" msgstr "Página oficial" @@ -1456,43 +1138,32 @@ msgstr "Página oficial" msgid "Old password" msgstr "Antigua contraseña" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "" -"Una vez que todos tus archivos estén subidos, pulsa el siguiente botón para " -"comprobar el estado de la importación." - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 msgid "Open" msgstr "Abierta" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "Opciones" -#: front/src/components/library/import/Main.vue:93 -msgid "Or" -msgstr "O" - #: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 src/components/library/Radios.vue:33 +#: src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" msgstr "Orden" #: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 src/components/library/Radios.vue:41 +#: src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "Dirección del orden" @@ -1500,10 +1171,6 @@ msgstr "Dirección del orden" msgid "Owner" msgstr "Propietario" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "Página no encontrada" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "¡Página no encontrada!" @@ -1512,42 +1179,26 @@ msgstr "¡Página no encontrada!" msgid "Password" msgstr "Contraseña" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "Contraseña actualizada" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "Contraseña actualizada con éxito" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "Pausar la canción" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "En espera" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" msgstr "Aprobación pendiente" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" -msgstr "Solicitudes de seguimiento pendientes" +#: front/src/views/content/libraries/Quota.vue:22 +#, fuzzy +msgid "Pending files" +msgstr "En espera" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "Solicitudes de importación pendientes" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "Solicitudes pendientes" @@ -1556,13 +1207,14 @@ msgstr "Solicitudes pendientes" msgid "Permissions" msgstr "Permisos" -#: front/src/components/audio/PlayButton.vue:9 src/components/library/Track.vue:30 +#: front/src/components/audio/PlayButton.vue:9 +#: src/components/library/Track.vue:30 msgid "Play" msgstr "Reproducir" #: front/src/components/audio/album/Card.vue:50 -#: front/src/components/audio/artist/Card.vue:44 src/components/library/Album.vue:28 -#: front/src/views/playlists/Detail.vue:23 +#: front/src/components/audio/artist/Card.vue:44 +#: src/components/library/Album.vue:28 front/src/views/playlists/Detail.vue:23 msgid "Play all" msgstr "Reproducir todo" @@ -1570,10 +1222,6 @@ msgstr "Reproducir todo" msgid "Play all albums" msgstr "Reproducir todos los álbumes" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "Reproducir inmediatamente" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "Reproducir siguiente" @@ -1582,14 +1230,6 @@ msgstr "Reproducir siguiente" msgid "Play now" msgstr "Reproducir ahora" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "Reproducir canción" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "Lista de reproducción" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1616,9 +1256,9 @@ msgstr "Lista de reproducción actualizada" msgid "Playlist visibility" msgstr "Visibilidad de lista de reproducción" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" msgstr "Listas de reproducción" @@ -1632,50 +1272,51 @@ msgstr "Por favor, comprueba que tu contraseña es correcta" #: front/src/components/auth/Login.vue:9 msgid "Please double-check your username/password couple is correct" -msgstr "" -"Por favor, comprueba que tu nombre de usuarix y contraseña son correctos" +msgstr "Por favor, comprueba que tu nombre de usuarix y contraseña son correctos" #: front/src/components/auth/Settings.vue:46 msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." msgstr "PNG, GIF o JPG. Máximo de 2MB. La imagen será reducida a 400x400px." -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "Paso anterior" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" -msgstr "Canción anterior" +#: front/src/components/library/FileUpload.vue:58 +#, fuzzy +msgid "Proceed" +msgstr "Proceder a inicio de sesión" #: front/src/views/auth/EmailConfirm.vue:26 #: front/src/views/auth/PasswordResetConfirm.vue:31 msgid "Proceed to login" msgstr "Proceder a inicio de sesión" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" -msgstr "Fecha de publicación" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" +msgstr "" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" -msgstr "Plantilla de consulta" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" +msgstr "" #: front/src/components/Sidebar.vue:20 msgid "Queue" msgstr "Cola de reproducción" -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "¡Cola de reproducción mezclada!" - -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "Radio" - -#: front/src/components/library/radios/Builder.vue:226 -msgid "Radio Builder" -msgstr "Editor de radio" - #: front/src/components/library/radios/Builder.vue:15 msgid "Radio created" msgstr "Radio creada" @@ -1688,7 +1329,8 @@ msgstr "Nombre de la radio" msgid "Radio updated" msgstr "Radio actualizada" -#: front/src/components/library/Library.vue:10 src/components/library/Radios.vue:141 +#: front/src/components/library/Library.vue:10 +#: src/components/library/Radios.vue:142 msgid "Radios" msgstr "Radios" @@ -1708,33 +1350,40 @@ msgstr "Añadidos a favoritos recientemente" msgid "Recently listened" msgstr "Escuchados recientemente" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" msgstr "Recargar" -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "Rechazado" - #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" msgstr "Inscritx desde %{ date }" #: front/src/components/auth/Signup.vue:9 msgid "Registration are closed on this instance, you will need an invitation code to signup." -msgstr "" -"La inscripción a esta instancia está cerrada, necesitarás un código de " -"invitación para inscribirte." +msgstr "La inscripción a esta instancia está cerrada, necesitarás un código de invitación para inscribirte." #: front/src/components/manage/users/UsersTable.vue:71 msgid "regular user" msgstr "Usuarix estándar" +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "" + +#: front/src/views/content/remote/Home.vue:6 +#, fuzzy +msgid "Remote libraries" +msgstr "Llévame a la biblioteca" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." +msgstr "" + #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" msgstr "Quitar" @@ -1743,10 +1392,6 @@ msgstr "Quitar" msgid "Remove avatar" msgstr "Quitar avatar" -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "Quitar de favoritos" - #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" @@ -1760,148 +1405,94 @@ msgstr "¿Solicitar una nueva contraseña de la API Subsonic?" msgid "Request a password" msgstr "Solicitar una contraseña" -#: front/src/App.vue:35 -msgid "Request music" -msgstr "Solicitar música" - -#: front/src/views/library/MusicRequest.vue:4 src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "Solicitar música" - #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" msgstr "¡Solicitud enviada!" -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "Reiniciar las tareas con errores" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "Reiniciar tarea" - #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" msgstr "Restablecer tu contraseña" -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "Resultado %{ current }/%{ total }" - -#: front/src/components/favorites/List.vue:38 src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/favorites/List.vue:38 +#: src/components/library/Artists.vue:30 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" msgstr "Resultados por página" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:29 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" msgstr "Guardar" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" -msgstr "Escaneo iniciado!" +#: front/src/views/content/remote/Card.vue:31 +#, fuzzy +msgid "Scan pending" +msgstr "Ascendente" -#: front/src/components/federation/LibraryTrackTable.vue:5 -#: front/src/components/library/Artists.vue:10 src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 +#: front/src/views/content/remote/Card.vue:43 +#, fuzzy +msgid "Scanned successfully" +msgstr "Ajustes actualizados con éxito." + +#: front/src/views/content/remote/Card.vue:47 +#, fuzzy +msgid "Scanned with errors" +msgstr "Cambios sincronizados con el servidor" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" +msgstr "" + +#: front/src/components/library/Artists.vue:10 +#: src/components/library/Radios.vue:29 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 src/views/playlists/List.vue:13 +#: front/src/views/content/libraries/FilesTable.vue:5 +#: src/views/playlists/List.vue:13 msgid "Search" msgstr "Buscar" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "Buscar una entidad que te gustaría importar:" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "Buscar por artista, nombre de usuarix, comentario…" - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "Buscar por fuente…" - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "Buscar por remitente, fuente…" - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "Buscar por título, artista, dominio…" - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "Buscar por nombre de usuarix, dominio…" - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "Buscar por nombre de usuarix, dirección de correo electrónico, código…" - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "Buscar por nombre de usuarix, dirrección de correo electrónico, nombre…" - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." -msgstr "Buscar artistas, álbumes, canciones…" +#: front/src/views/content/remote/ScanForm.vue:9 +#, fuzzy +msgid "Search a remote library" +msgstr "Llévame a la biblioteca" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" msgstr "Buscar música" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" msgstr "Buscar en lyrics.wikia.com" -#: front/src/components/library/Album.vue:33 src/components/library/Artist.vue:31 +#: front/src/components/library/Album.vue:33 +#: src/components/library/Artist.vue:31 #: front/src/components/library/Track.vue:40 msgid "Search on Wikipedia" msgstr "Buscar en Wikipedia" -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "Búsqueda" - #: front/src/views/admin/Settings.vue:15 msgid "Sections" msgstr "Secciones" -#: front/src/components/library/radios/Builder.vue:41 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" msgstr "Seleccionar un filtro" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" msgstr[0] "Seleccionar %{ total } elemento" msgstr[1] "Seleccionar los %{ total } elementos" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "Seleccionar archivos para subir…" - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" msgstr "Seleccionar solo la página actual" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "Seleccionar las fuentes o archivos para importar" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "Enviar una solicitud de seguimiento" - -#: front/src/components/Sidebar.vue:97 src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" msgstr "Ajustes" @@ -1913,99 +1504,89 @@ msgstr "Ajustes actualizados" msgid "Settings updated successfully." msgstr "Ajustes actualizados con éxito." -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "Ajustes…" - #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" msgstr "Compartir enlace" +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +#, fuzzy +msgid "Sharing link" +msgstr "Compartir enlace" + +#: front/src/components/audio/album/Card.vue:40 +#, fuzzy +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "%{ count } canción" +msgstr[1] "%{ count } canciones" + #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" msgstr[0] "Mostrar 1 álbum adicional" msgstr[1] "Mostrar %{ count } álbumes adicionales" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "Mostrar 1 canción adicional" -msgstr[1] "Mostrar %{ count } canciones adicionales" +#: front/src/views/Notifications.vue:10 +#, fuzzy +msgid "Show read notifications" +msgstr "Última modificación" -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" -msgstr "Mostrar/ocultar contraseña" - -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" msgstr "Mostrando resultados %{ start }-%{ end } de %{ total }" -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "Mezclar tu cola de reproducción" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "Inscripción" - #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" msgstr "Inscripción" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "Copia y pega el texto de abajo en un terminal para iniciar la descarga." - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 +#: src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" msgstr "Tamaño" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 msgid "Skipped" msgstr "Omitido" +#: front/src/views/content/libraries/Quota.vue:49 +#, fuzzy +msgid "Skipped files" +msgstr "Omitido" + #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "¿Falta algo en la biblioteca? Dinos lo que te gustaría escuchar!" #: front/src/components/audio/Search.vue:25 msgid "Sorry, we did not found any album matching your query" -msgstr "" -"Lo sentimos, no hemos encontrado ningún álbum que corresponda con tu búsqueda" +msgstr "Lo sentimos, no hemos encontrado ningún álbum que corresponda con tu búsqueda" #: front/src/components/audio/Search.vue:16 msgid "Sorry, we did not found any artist matching your query" -msgstr "" -"Lo sentimos, no hemos encontrado ningún artista que corresponda con tu " -"búsqueda" +msgstr "Lo sentimos, no hemos encontrado ningún artista que corresponda con tu búsqueda" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "Fuente" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "Código fuente" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "Código fuente (%{version})" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" msgstr "Miembro del equipo" @@ -2014,26 +1595,11 @@ msgstr "Miembro del equipo" msgid "Start" msgstr "Iniciar" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "Iniciar subida" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "Estadísticas" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "Estado" @@ -2041,15 +1607,11 @@ msgstr "Estado" msgid "Stop" msgstr "Parar" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" msgstr "Parar radio" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "Parar subida" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" msgstr "Enviar" @@ -2057,98 +1619,85 @@ msgstr "Enviar" msgid "Submit another request" msgstr "Enviar otra solicitud" -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "Presentado por" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "Subsonic" - #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" msgstr "Contraseña de la API Subsonic" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" -msgstr "Éxito" - -#: front/src/App.vue:11 +#: front/src/App.vue:13 msgid "Suggested choices" msgstr "Sugerencias" +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" +msgstr "" + #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "Sincronizando los cambios con el servidor…" +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." -msgstr "" -"Así de simple: nos encantó Grooveshark y quisimos hacer algo incluso mejor." +msgstr "Así de simple: nos encantó Grooveshark y quisimos hacer algo incluso mejor." -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "Los Beatles, Michael Jackson…" - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." -msgstr "" -"El logotipo de funkwhale fue generosamente diseñado y suplido por Francis " -"Gading." +msgstr "El logotipo de funkwhale fue generosamente diseñado y suplido por Francis Gading." -#: front/src/components/Home.vue:124 -msgid "The plaform is free and open-source, you can install it and modify it without worries" +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." msgstr "" -"La plataforma es gratuita y de código fuente libre, puedes instalarla y " -"modificarla sin restricciones" + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "" + +#: front/src/components/Home.vue:121 +msgid "The plaform is free and open-source, you can install it and modify it without worries" +msgstr "La plataforma es gratuita y de código fuente libre, puedes instalarla y modificarla sin restricciones" #: front/src/components/auth/SubsonicTokenForm.vue:4 msgid "The Subsonic API is not available on this Funkwhale instance." msgstr "La API Subsonic no está disponible en esta instancia de Funkwhale." -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" -msgstr "El Álbum Blanco, Thriller…" - -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" msgstr "" -"Actualmente no existe una forma de descargar múltiples canciones " -"directamente de funkwhale en un archivo ZIP. Pero, puedes usar herramientas " -"de linea de comando como cURL para descargar una lista de canciones " -"fácilmente." -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" msgstr "" -"Esta importación se asociará con la solicitud de música de abajo. Después de " -"terminar la importación, la solicitud será marcada como completada." -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" -msgstr "Esto indica si la biblioteca te ha concedido el acceso" +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" +msgstr "" + +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." +msgstr "" #: front/src/components/auth/Profile.vue:16 msgid "This is you!" msgstr "¡Eres tú!" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." -msgstr "" -"Esto puede afectar a muchos elementos, por favor comprueba si esto es " -"realmente lo que quieres." +msgstr "Esto puede afectar a muchos elementos, por favor comprueba si esto es realmente lo que quieres." -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" -msgstr "Esta canción no está importada y no puede ser reproducida" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "" + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" +msgstr "" #: front/src/views/playlists/Detail.vue:37 msgid "This will completely delete this playlist and cannot be undone." -msgstr "" -"Esto borrará por completo esta lista de reproducción y no se podrá deshacer." +msgstr "Esto borrará por completo esta lista de reproducción y no se podrá deshacer." #: front/src/views/radios/Detail.vue:27 msgid "This will completely delete this radio and cannot be undone." @@ -2156,36 +1705,194 @@ msgstr "Esto borrará por completo esta radio y no se podrá deshacer." #: front/src/components/auth/SubsonicTokenForm.vue:51 msgid "This will completely disable access to the Subsonic API using from account." -msgstr "" -"Esto desactivará por completo el acceso a la API Subsonic desde esta cuenta." +msgstr "Esto desactivará por completo el acceso a la API Subsonic desde esta cuenta." -#: front/src/App.vue:137 +#: front/src/App.vue:162 src/components/About.vue:55 +#: src/components/Home.vue:154 front/src/components/PageNotFound.vue:33 +#: src/components/Sidebar.vue:203 front/src/components/Sidebar.vue:204 +#: src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 +#: src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 +#: src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 +#: src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 +#: src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 +#: src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 +#: src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 +#: src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 +#: src/components/audio/SearchBar.vue:27 front/src/components/auth/Login.vue:77 +#: src/components/auth/Login.vue:78 front/src/components/auth/Logout.vue:20 +#: src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 +#: src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 +#: src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 +#: src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 +#: src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 +#: src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 +#: src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 +#: src/views/auth/EmailConfirm.vue:51 front/src/views/auth/PasswordReset.vue:52 +#: src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +#: src/views/content/Base.vue:19 front/src/views/content/Home.vue:35 +#: src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 +#: src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 +#: src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 +#: src/views/instance/Timeline.vue:57 front/src/views/playlists/Detail.vue:90 +#: src/views/playlists/List.vue:104 front/src/views/playlists/List.vue:105 +#: src/views/radios/Detail.vue:80 +#, fuzzy msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "Esto borrará tus datos locales y te desconectará, ¿quieres continuar?" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "Esto borrará tus datos locales y te desconectará, ¿quieres continuar?" +msgstr[1] "Esto borrará tus datos locales y te desconectará, ¿quieres continuar?" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." -msgstr "" -"Esto cerrará tus sesiones en todos los dispositivos que usan esa contraseña." +msgstr "Esto cerrará tus sesiones en todos los dispositivos que usan esa contraseña." #: front/src/components/playlists/Editor.vue:44 msgid "This will remove all tracks from this playlist and cannot be undone." +msgstr "Esto borrará todas las canciones de esta lista de reproducción y no se podrá deshacer." + +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." msgstr "" -"Esto borrará todas las canciones de esta lista de reproducción y no se podrá " -"deshacer." #: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 #: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 msgid "Title" msgstr "Título" -#: front/src/components/audio/SearchBar.vue:27 src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" -msgstr "Canción" - #: front/src/components/library/Track.vue:53 msgid "Track information" msgstr "Información de la canción" @@ -2200,15 +1907,11 @@ msgstr "Canciones" #: front/src/components/library/Album.vue:43 #: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 -#: front/src/views/radios/Detail.vue:34 +#: src/views/content/Base.vue:8 front/src/views/content/libraries/Detail.vue:8 +#: src/views/playlists/Detail.vue:50 front/src/views/radios/Detail.vue:34 msgid "Tracks" msgstr "Canciones" -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "Canciones disponibles en esta biblioteca" - #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" msgstr "Canciones de este artista" @@ -2221,36 +1924,39 @@ msgstr "Canciones en favoritos" msgid "tracks listened" msgstr "Canciones escuchadas" -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "Iniciar escaneo" - +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" msgstr "Tipo" +#: front/src/views/content/remote/Card.vue:100 +#: src/views/content/remote/Card.vue:105 +#, fuzzy +msgid "Unfollow" +msgstr "Seguir" + +#: front/src/views/content/remote/Card.vue:101 +#, fuzzy +msgid "Unfollow this library?" +msgstr "Llévame a la biblioteca" + #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." -msgstr "" -"Desafortunadamente, lxs propietarixs de esta instancia aún no han tomado el " -"tiempo para completar esta página." - -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "Desconocido" +msgstr "Desafortunadamente, lxs propietarixs de esta instancia aún no han tomado el tiempo para completar esta página." #: front/src/components/Home.vue:37 msgid "Unlimited music" msgstr "Música ilimitada" -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "Activar sonido" - #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" msgstr "Actualizar avatar" +#: front/src/views/content/libraries/Form.vue:25 +#, fuzzy +msgid "Update library" +msgstr "Gestionar biblioteca" + #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" msgstr "Actualizar lista de reproducción" @@ -2263,8 +1969,10 @@ msgstr "Actualizar ajustes" msgid "Update your password" msgstr "Actualizar tu contraseña" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" msgstr "Subir" @@ -2272,46 +1980,57 @@ msgstr "Subir" msgid "Upload a new avatar" msgstr "Subir un nuevo avatar" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" -msgstr "Archivos subidos o fuente externa" +#: front/src/views/content/Home.vue:6 +#, fuzzy +msgid "Upload audio content" +msgstr "Subir un nuevo avatar" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/views/content/libraries/FilesTable.vue:54 +#, fuzzy +msgid "Upload date" +msgstr "Subir" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:31 +#, fuzzy +msgid "Upload new tracks" +msgstr "Subir un nuevo avatar" + +#: front/src/views/admin/users/UsersDetail.vue:82 +#, fuzzy +msgid "Upload quota" +msgstr "Subir" + +#: front/src/components/library/FileUpload.vue:99 +#, fuzzy +msgid "Uploaded" +msgstr "Subir" + +#: front/src/components/library/FileUpload.vue:5 +#, fuzzy +msgid "Uploading" +msgstr "Subiendo…" + +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "Subiendo…" -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" msgstr "Usar otra instancia" -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "" -"Usa este campo de comentarios para añadir detalles a tu solicitud si hace " -"falta" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "" -"Usa este ajuste para activar/desactivar la federación con esta biblioteca" - #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." -msgstr "" -"Usa este formulario para solicitar un restablecimiento de contraseña. Te " -"mandaremos un correo electrónico a la dirección proporcionada con " -"instrucciones para restablecer tu contraseña." - -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "" -"Usa esta formulario para escanear una instancia y establecer la federación." +msgstr "Usa este formulario para solicitar un restablecimiento de contraseña. Te mandaremos un correo electrónico a la dirección proporcionada con instrucciones para restablecer tu contraseña." #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" msgstr "Usado" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" msgstr "Usuarix" @@ -2319,12 +2038,20 @@ msgstr "Usuarix" msgid "User activity" msgstr "Actividad de usuarix" +#: front/src/components/library/Album.vue:49 +#: src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +#, fuzzy +msgid "User libraries" +msgstr "Bibliotecas" + #: front/src/components/library/Radios.vue:20 msgid "User radios" msgstr "Radios de usuarixs" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" msgstr "Nombre de usuarix" @@ -2336,20 +2063,32 @@ msgstr "Nombre de usuarix o correo electónico" msgid "users" msgstr "usuarix" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 +#: src/views/admin/users/UsersList.vue:21 front/src/views/admin/Settings.vue:81 msgid "Users" msgstr "Usuarix" -#: front/src/components/library/Album.vue:37 src/components/library/Artist.vue:35 +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +#, fuzzy +msgid "View files" +msgstr "Archivos de biblioteca" + +#: front/src/components/library/Album.vue:37 +#: src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" msgstr "Ver en MusicBrainz" +#: front/src/views/content/libraries/Form.vue:18 +#, fuzzy +msgid "Visibility" +msgstr "Visibilidad de lista de reproducción" + #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" msgstr "No podemos añadir la canción a una lista de reproducción" @@ -2374,12 +2113,21 @@ msgstr "No podemos guardar tu avatar" msgid "We cannot save your settings" msgstr "No podemos guardar tus ajustes" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" msgstr "No te rastreamos y no te molestamos con anuncios" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:40 +#, fuzzy +msgid "we recommend using Picard for that purpose" msgstr "Recomendamos usar Picard para eso." #: front/src/components/Home.vue:7 @@ -2394,29 +2142,15 @@ msgstr "Lo sentimos, la página solicitada no existe:" msgid "We've received your request, you'll get some groove soon ;)" msgstr "Hemos recibido tu solicitud, pronto tendrás algo sabroso ;)" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "Bienvenidx" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" msgstr "Bienvenidx a Funkwhale" -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "¿Qué son los metadatos?" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "" -"Cuando activas este ajuste, las nuevas canciones publicadas en esta " -"biblioteca se importarán de forma automática" - #: front/src/components/Home.vue:24 msgid "Why funkwhale?" msgstr "¿Por qué funkwhale?" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" msgstr "Sí" @@ -2424,82 +2158,490 @@ msgstr "Sí" msgid "Yes, log me out!" msgstr "Sí, cierra mi sesión!" +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" +msgstr "" + #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" msgstr "Tienes iniciada actualmente sesión como %{ username }" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." -msgstr "También puedes omitir este paso e introducir los metadatos manualmente." - -#: front/src/components/Home.vue:136 -msgid "You can invite friends and family to your instance so they can enjoy your music" +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." msgstr "" -"Puedes invitar a tus amigxs y tu familia a tu instancia para que ellxs " -"puedan disfrutar de tu música" + +#: front/src/components/Home.vue:133 +msgid "You can invite friends and family to your instance so they can enjoy your music" +msgstr "Puedes invitar a tus amigxs y tu familia a tu instancia para que ellxs puedan disfrutar de tu música" #: front/src/components/library/radios/Builder.vue:7 msgid "You can use this interface to build your own custom radio, which will play tracks according to your criteria." -msgstr "" -"Puedes usar esta interfaz para hacer tu propia radio personalizada, que " -"reproducirá canciones según tus criterios." +msgstr "Puedes usar esta interfaz para hacer tu propia radio personalizada, que reproducirá canciones según tus criterios." #: front/src/components/auth/SubsonicTokenForm.vue:8 msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." -msgstr "" -"Puedes usarlos para disfrutar de tus listas de reproducción y tu música en " -"modo sin conexión, en tu smartphone o tu tablet, por ejemplo." +msgstr "Puedes usarlos para disfrutar de tus listas de reproducción y tu música en modo sin conexión, en tu smartphone o tu tablet, por ejemplo." -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" msgstr "Estás escuchando una radio" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" msgstr "Tienes que seleccionar una instancia para poder continuar" +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." +msgstr "" + #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" -msgstr "" -"Se cerrará esta sesión y tendrás que reiniciar sesión con la nueva contraseña" +msgstr "Se cerrará esta sesión y tendrás que reiniciar sesión con la nueva contraseña" #: front/src/components/auth/Settings.vue:71 msgid "You will have to update your password on your clients that use this password." -msgstr "" -"Tendrás que actualizar tu contraseña en los clientes que usan esta " -"contraseña." - -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "Vas a importar:" +msgstr "Tendrás que actualizar tu contraseña en los clientes que usan esta contraseña." #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." -msgstr "" -"Tu dirección de correo electrónico ha sido confirmada, ahora puedes usar el " -"servicio sin limitaciones." +msgstr "Tu dirección de correo electrónico ha sido confirmada, ahora puedes usar el servicio sin limitaciones." -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" -msgstr "Tus favoritos" - -#: front/src/components/Home.vue:117 +#: front/src/components/Home.vue:114 msgid "Your music, your way" msgstr "Tu música, a tu manera" +#: front/src/views/Notifications.vue:7 +#, fuzzy +msgid "Your notifications" +msgstr "Última modificación" + #: front/src/views/auth/PasswordResetConfirm.vue:29 msgid "Your password has been updated successfully." msgstr "Tu contraseña ha sido cambiada con éxito." #: front/src/components/auth/Settings.vue:101 msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" -msgstr "" -"Tu contraseña Subsonic se cambiará a una nueva contraseña aleatoria, " -"cerrando tus sesiones en los dispositivos que usaban la antigua contraseña " -"Subsonic" +msgstr "Tu contraseña Subsonic se cambiará a una nueva contraseña aleatoria, cerrando tus sesiones en los dispositivos que usaban la antigua contraseña Subsonic" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +#, fuzzy +msgid "Activity visibility" +msgstr "Visibilidad de lista de reproducción" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 +#: src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "Solo yo" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 +#: src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "Todo el mundo en esta instancia" + +#: front/src/components/mixins/Translations.vue:18 +#, fuzzy +msgid "Accessed date" +msgstr "Acceso deshabilitado" + +#: front/src/components/mixins/Translations.vue:19 +#, fuzzy +msgid "Modification date" +msgstr "Fecha de caducidad" + +#: front/src/components/mixins/Translations.vue:20 +#, fuzzy +msgid "Imported date" +msgstr "Fecha de importación" + +#: front/src/components/mixins/Translations.vue:22 +#, fuzzy +msgid "Track name" +msgstr "Canción" + +#: front/src/components/mixins/Translations.vue:23 +#, fuzzy +msgid "Album name" +msgstr "Página del álbum" + +#: front/src/components/mixins/Translations.vue:30 +#, fuzzy +msgid "Sign-up date" +msgstr "Inscripción" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "Copiar canciones de cola de reproducción actual a lista de reproducción" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "Añadir a esta lista de reproducción" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "Mi lista de reproducción molona" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "Todo el mundo" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "Inscripción" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "Introducir tu código de invitación (no distingue mayúsculas de minúsculas)" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "Introducir tu nombre de usuario" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "Introducir tu correo electrónico" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "Contraseña actualizada" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "Acceso deshabilitado" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "Introducir tu nombre de usuario o correo electrónico" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "Iniciar sesión" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "Perfil de %{ username }" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "Cerrar sesión" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "Configuración de Cuenta" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "Quitar de favoritos" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "Tus favoritos" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "Introducir un nombre de radio..." + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "Editor de radio" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "Mi radio molona" + +#: front/src/components/library/radios/Builder.vue:236 +#, fuzzy +msgid "My awesome description" +msgstr "Mi radio molona" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "" + +#: front/src/components/library/FileUpload.vue:240 +#, fuzzy +msgid "A network error occured while uploading this file" +msgstr "Ha ocurrido un error al guardar los cambios" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "Introducir un nombre de artista..." + +#: front/src/components/library/Track.vue:195 +#: src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "Canción" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "Inicio" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "Mostrar/ocultar contraseña" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "Los Beatles, Michael Jackson…" + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "El Álbum Blanco, Thriller…" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "Usa este campo de comentarios para añadir detalles a tu solicitud si hace falta" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" msgstr[0] "%{ count } canción ha sido añadida a tu cola de reproducción" msgstr[1] "%{ count } canciones han sido añadidas a tu cola de reproducción" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "Artista, álbum, canción..." + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "Buscar artistas, álbumes, canciones…" + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "¡Cola de reproducción mezclada!" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "Canción anterior" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "Reproducir canción" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "Pausar la canción" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "Próxima canción" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "Activar sonido" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "Silencio" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "Bucle deshabilitado. Pulsa para cambiar a reproducción en bucle de la canción actual." + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "Bucle de la canción actual. Pulsa para cambiar a la reproducción en bucle de la cola de reproducción entera." + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "Bucle de la cola de reproducción entera. Pulsa para desactivar la reproducción en bucle." + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "Mezclar tu cola de reproducción" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "Vaciar cola de reproducción" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "Solicitudes de importación pendientes" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "Solicitudes de seguimiento pendientes" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "Introducir tu búsqueda..." + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "Buscar por título, artista, dominio…" + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "Dejar en blanco para un código aleatorio" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "Buscar por nombre de usuarix, dirección de correo electrónico, código…" + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "Buscar por nombre de usuarix, dirrección de correo electrónico, nombre…" + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "Federación" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "" + +#: front/src/views/content/remote/ScanForm.vue:48 +#, fuzzy +msgid "Enter a library url" +msgstr "Introducir un nombre de dominio de biblioteca..." + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:226 +#, fuzzy +msgid "Search by title, artist, album..." +msgstr "Buscar por título, artista, dominio…" + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:242 +#, fuzzy +msgid "Import went on successfully" +msgstr "Contraseña actualizada con éxito" + +#: front/src/views/content/libraries/FilesTable.vue:259 +#, fuzzy +msgid "Relaunch import" +msgstr "Terminar importación" + +#: front/src/views/content/libraries/Card.vue:58 +#, fuzzy +msgid "Visibility: nobody except me" +msgstr "Solo yo" + +#: front/src/views/content/libraries/Card.vue:59 +#, fuzzy +msgid "Visibility: everyone on this instance" +msgstr "Todo el mundo en esta instancia" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:61 +#, fuzzy +msgid "Total size of the files in this library" +msgstr "Canciones disponibles en esta biblioteca" + +#: front/src/views/content/libraries/Form.vue:70 +#, fuzzy +msgid "My awesome library" +msgstr "Mi radio molona" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:74 +#, fuzzy +msgid "Everyone, including other instances" +msgstr "Todo el mundo en esta instancia" + +#: front/src/views/content/libraries/Form.vue:106 +#, fuzzy +msgid "Library updated" +msgstr "Nombre de biblioteca" + +#: front/src/views/content/libraries/Form.vue:109 +#, fuzzy +msgid "Library created" +msgstr "Nombre de biblioteca" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "Radio" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "Linea de tiempo de la instancia" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "Lista de reproducción" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "Introducir un nombre de lista de reproducción..." + +#: front/src/views/admin/library/Base.vue:16 +#, fuzzy +msgid "Manage library" +msgstr "En biblioteca" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "Determina si la cuenta de usuario es activa o no. Los usuarios inactivos no pueden conectarse ni usar el servicio." + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "" + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "Gestionar usuarixs" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "Ajustes de la instancia" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "Información de esta instancia" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "Subsonic" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "Estadísticas" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "Informes de error" diff --git a/front/locales/fr_FR/LC_MESSAGES/app.po b/front/locales/fr_FR/LC_MESSAGES/app.po index 384131ca2..2fc1d13c5 100644 --- a/front/locales/fr_FR/LC_MESSAGES/app.po +++ b/front/locales/fr_FR/LC_MESSAGES/app.po @@ -2,9 +2,9 @@ msgid "" msgstr "" "Project-Id-Version: French (Funkwhale)\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-22 14:12+0200\n" -"PO-Revision-Date: 2018-07-30 16:28+0000\n" -"Last-Translator: Eliot Berriot \n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" +"PO-Revision-Date: 2018-10-07 09:06+0000\n" +"Last-Translator: troll \n" "Language-Team: French \n" "Language: fr_FR\n" @@ -26,15 +26,16 @@ msgstr "(%{ index } sur %{ length })" msgid "(empty)" msgstr "(vide)" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "%{ count } sur %{ total } élément sélectionné" msgstr[1] "%{ count } sur %{ total } éléments sélectionnés" -#: front/src/components/Sidebar.vue:116 -#: src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 +#: src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" msgstr[0] "%{ count } piste" @@ -46,7 +47,7 @@ msgid_plural "%{ count } tracks in %{ albumsCount } albums" msgstr[0] "%{ count } piste dans %{ albumsCount } albums" msgstr[1] "%{ count } pistes dans %{ albumsCount } albums" -#: front/src/components/library/radios/Builder.vue:76 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" msgstr[0] "%{ count } piste correspondant aux filtres sélectionnés" @@ -58,6 +59,10 @@ msgid_plural "%{ count } tracks" msgstr[0] "%{ count} piste" msgstr[1] "%{ count } pistes" +#: front/src/views/content/libraries/Quota.vue:11 +msgid "%{ current } used on %{ max } allowed" +msgstr "%{ current } utilisé sur %{ max } autorisé" + #: front/src/components/common/Duration.vue:2 msgid "%{ hours } h %{ minutes } min" msgstr "%{hours} h %{minutes} min" @@ -74,10 +79,6 @@ msgstr "%{ user } a ajouté une piste a ses favoris" msgid "%{ user } listened to a track" msgstr "%{ user } a écouté une piste" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "Profil de %{ username }" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" @@ -90,35 +91,27 @@ msgid_plural "%{ count } favorites" msgstr[0] "1 favoris" msgstr[1] "%{ count } favoris" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "1 piste" -msgstr[1] "%{ count } pistes" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "À propos de %{instance}" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "À propos de Funkwhale" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "A propos de cette instance" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 +#: front/src/views/content/libraries/Detail.vue:48 +msgid "Accept" +msgstr "Accepter" + +#: front/src/views/content/libraries/Detail.vue:40 msgid "Accepted" msgstr "Accepté" -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "Accès désactivé" - -#: front/src/components/Home.vue:109 +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" msgstr "Accédez à votre musique depuis une interface épurée, adaptée à ce qui compte vraiment" @@ -130,10 +123,6 @@ msgstr "Compte actif" msgid "Account settings" msgstr "Paramètres du compte" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "Paramètres du compte" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "Statut du compte" @@ -142,16 +131,18 @@ msgstr "Statut du compte" msgid "Account's email" msgstr "Email du compte" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +msgid "Action" +msgstr "Action" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" msgstr[0] "L'action %{ action } a été lancée avec succès sur %{ count } élément" msgstr[1] "L'action %{ action } a été lancée avec succès sur %{ count } éléments" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:60 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "Actions" @@ -159,30 +150,22 @@ msgstr "Actions" msgid "Active" msgstr "Actif" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "Activité" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" -msgstr "Acteur" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" +msgstr "Ajouter du contenu" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "Ajouter une nouvelle bibliothèque" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" msgstr "Ajouter des filtres" -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" msgstr "Ajouter des filtres pour personnaliser votre radio" -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "Ajouter à la queue" - #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" @@ -197,10 +180,6 @@ msgstr "Ajouter à une playlist..." msgid "Add to queue" msgstr "Ajouter à la queue" -#: front/src/components/playlists/PlaylistModal.vue:116 -msgid "Add to this playlist" -msgstr "Ajouter à cette playlist" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "Ajouter une piste" @@ -209,25 +188,19 @@ msgstr "Ajouter une piste" msgid "Admin" msgstr "Admin" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" msgstr "Administration" -#: front/src/components/audio/SearchBar.vue:26 -#: src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 +#: src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "Album" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "Album %{ title } (%{ count } piste) par %{ artist }" -msgstr[1] "Album %{ title } (%{ count } pistes) by %{ artist }" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" @@ -240,7 +213,6 @@ msgstr "Page de l'album" #: front/src/components/audio/Search.vue:19 #: src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "Albums" @@ -249,8 +221,8 @@ msgstr "Albums" msgid "Albums by this artist" msgstr "Albums de cet·te artiste" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "Tout" @@ -260,49 +232,23 @@ msgstr "Une erreur s'est produite pendant l'enregistrement de vos modifications" #: front/src/components/auth/Login.vue:10 msgid "An unknown error happend, this can mean the server is down or cannot be reached" -msgstr "" -"Une erreur inconnue s'est produite, cela pourrait signifier que le serveur " -"ne peut pas être accedé" - -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "Tous" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "API" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "Approuver" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "Approuver l'accès ?" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "Approuvé" +msgstr "Une erreur inconnue s'est produite, cela pourrait signifier que le serveur ne peut pas être atteint" #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "Êtes-vous sur de vouloir vous déconnecter ?" -#: front/src/components/audio/SearchBar.vue:25 -#: src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 +#: src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" msgstr "Artiste" #: front/src/components/requests/Form.vue:5 +#: src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "Nom de l'artiste" @@ -311,26 +257,20 @@ msgstr "Nom de l'artiste" msgid "Artist page" msgstr "Page de l'artiste" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "Artiste, album, piste..." - #: front/src/components/audio/Search.vue:10 #: src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 -#: src/components/library/Library.vue:7 +#: front/src/components/library/Library.vue:7 +#: src/components/library/Artists.vue:120 msgid "Artists" msgstr "Artistes" #: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 -#: src/components/library/Radios.vue:44 +#: src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 -#: src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" msgstr "Ascendant" @@ -338,10 +278,6 @@ msgstr "Ascendant" msgid "Ask for a password reset" msgstr "Demander à réinitialiser votre mot de passe" -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "Import automatique" - #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" msgstr "Playlists disponibles" @@ -355,12 +291,9 @@ msgstr "Avatar" msgid "Back to login" msgstr "Retour à la page de connexion" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "Soyez prudent·es en acceptant les demandes d'abonnements, car vos abonné·es pourront accéder à l'intégralité de votre bibliothèque." - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "Bitrate" @@ -368,7 +301,7 @@ msgstr "Bitrate" msgid "Browse" msgstr "Parcourir" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "Parcourir la bibliothèque" @@ -376,18 +309,6 @@ msgstr "Parcourir la bibliothèque" msgid "Browsing artists" msgstr "Parcourir les artistes" -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "Parcourir les pistes fédérées" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "Parcourir les abonnés" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "Parcourir les bibliothèques" - #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" msgstr "Parcourir les playlists" @@ -404,22 +325,18 @@ msgstr "Éditeur" msgid "By %{ artist }" msgstr "De %{ artist }" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." -msgstr "Si vous confirmez, %{ username } se verra refuser l'accès à votre bibliothèque." +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." +msgstr "" +"En arrêtant de suivre cette bibliothèque, vous perdrez l'accès à son contenu." -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "Si vous confirmez, %{ username } se verra accorder l'accès à votre bitliothèque." - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" msgstr "Annuler" -#: front/src/components/library/radios/Builder.vue:59 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "Pistes candidates" @@ -427,9 +344,9 @@ msgstr "Pistes candidates" msgid "Cannot change your password" msgstr "Mot de passe ne peut pas être changé" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" -msgstr "Changer de langue" +msgstr "Changer la langue" #: front/src/components/auth/Settings.vue:67 msgid "Change my password" @@ -460,7 +377,7 @@ msgstr "La mise à jour de votre mot de passe impactera également le mot de pas msgid "Changing your password will have the following consequences" msgstr "Modifier votre mot de passe aura les conséquences suivantes" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "Choisissez votre instance" @@ -477,22 +394,13 @@ msgstr "Effacer" msgid "Clear playlist" msgstr "Vider la playlist" -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "Vider la queue" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "CLI" - #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" msgstr "Écoutez de la musique pendant des heures, en un clic, grâce aux radios intégrées." -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" -msgstr "Fermée" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" +msgstr "Cliquer pour sélectionner les fichiers a téléverser ou glisser-déposser les fichiers ou répertoires" #: front/src/components/manage/users/InvitationForm.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:42 @@ -505,12 +413,11 @@ msgstr "Code" msgid "Collapse" msgstr "Réduire" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "Commentaire" -#: front/src/components/library/radios/Builder.vue:58 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" msgstr "Configuration" @@ -527,11 +434,11 @@ msgstr "Confirmer votre email" msgid "Confirmation code" msgstr "Code de confirmation" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" -msgstr "Copier les pistes de la queue dans la playlist" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" +msgstr "Copier" -#: front/src/components/Home.vue:88 +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" msgstr "Pochettes d'albums, paroles, notre but est de tout implémenter ;)" @@ -539,19 +446,26 @@ msgstr "Pochettes d'albums, paroles, notre but est de tout implémenter ;)" msgid "Create a funkwhale account" msgstr "Créer un compte funkwhale" +#: front/src/views/content/libraries/Home.vue:14 +msgid "Create a new library" +msgstr "Créer une nouvelle bibliothèque" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "Créer une nouvelle playlist" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "Créer un compte" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "Créer un import" +#: front/src/views/content/libraries/Form.vue:26 +msgid "Create library" +msgstr "Créer une bibliothèque" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "Créer mon compte" @@ -564,9 +478,8 @@ msgstr "Créer une playlist" msgid "Create your own radio" msgstr "Créer votre propre radio" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "Date de création" @@ -574,17 +487,34 @@ msgstr "Date de création" msgid "Current avatar" msgstr "Avatar actuel" +#: front/src/views/content/libraries/DetailArea.vue:4 +msgid "Current library" +msgstr "Bibliothèque actuelle" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "Piste actuelle" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +msgid "Current usage" +msgstr "Utilisation actuelle" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "Date" + +#: front/src/views/content/libraries/Form.vue:29 +#: src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "Supprimer" +#: front/src/views/content/libraries/Form.vue:39 +msgid "Delete library" +msgstr "Supprimer la bibliothèque" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "Supprimer la playlist" @@ -593,34 +523,32 @@ msgstr "Supprimer la playlist" msgid "Delete radio" msgstr "Supprimer la radio" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" -msgstr "Refuser" - -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "Refuser l'accès ?" +#: front/src/views/content/libraries/Form.vue:31 +msgid "Delete this library?" +msgstr "Supprimer cette bibliothèque ?" #: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 -#: src/components/library/Radios.vue:47 +#: src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 -#: src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "Descendant" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +msgid "Description" +msgstr "Description" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "Détail" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." -msgstr "Détermine si le compte utilisateur est actif ou non. Les comptes inactifs ne peuvent pas se connecter ou utiliser le service." +#: front/src/views/content/remote/Card.vue:50 +msgid "Details" +msgstr "Détails" #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 @@ -639,7 +567,7 @@ msgstr "Désactiver l'accès à l'API Subsonic ?" msgid "Discover how to use Funkwhale from other apps" msgstr "Découvrez comment utiliser Funkwhale sur d'autres applications" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "Afficher publiquement" @@ -656,9 +584,8 @@ msgid "Do you want to delete the playlist \"%{ playlist }\"?" msgstr "Voulez-vous supprimer la playlist \"%{ playlist }\" ?" #: front/src/views/radios/Detail.vue:26 -#, fuzzy msgid "Do you want to delete the radio \"%{ radio }\"?" -msgstr "Voulez-vous supprimer la radio \"%{ radio }\" ?" +msgstr "Voulez-vous supprimer la radio « %{ radio } » ?" #: front/src/components/common/ActionTable.vue:29 msgid "Do you want to launch %{ action } on %{ count } element?" @@ -666,41 +593,43 @@ msgid_plural "Do you want to launch %{ action } on %{ count } elements?" msgstr[0] "Voulez-vous effectuer l'action \"%{ action } sur %{ count } élément ?" msgstr[1] "Voulez-vous effectuer l'action \"%{ action } sur %{ count } éléments ?" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "Souhaitez-vous restaurer votre queue précédente ?" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "Documentation" -#: front/src/components/audio/track/Table.vue:24 -#: src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "Télécharger" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "Télécharger" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" msgstr "Glissez et déposer les lignes pour réordonner les pistes dans la playlist" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 +#: src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "Durée" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "Simple à utiliser" +#: front/src/views/content/libraries/Detail.vue:9 +msgid "Edit" +msgstr "Éditer" + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "Éditer les informations de cette instance" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "Éditer..." @@ -721,55 +650,16 @@ msgstr "Email confirmé" msgid "End edition" msgstr "Terminer l'édition" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "Vérifier que vos fichiers musicaux sont correctement taggués avant de les envoyer." - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "Saisissez un nom de radio..." - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "Saisissez un nom d'artiste..." - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "Saisissez un nom de domaine..." - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "Saisissez un nom de playlist..." - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "Saisissez votre email" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "Saisissez votre code d'invitation (insensible à la casse)" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "Saisissez votre recherche..." - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "Saisissez votre nom d'utilisateur·rice" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "Saisissez votre nom d'utilisateur·rice ou email" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "Erreur" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" -msgstr "Rapports d'erreur" +#: front/src/views/content/remote/Card.vue:39 +msgid "Error during scan" +msgstr "Erreur lors de l'analyse" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "Erreur lors du traitement de l'action" @@ -789,29 +679,28 @@ msgstr "Erreur lors de la confirmation de votre email" msgid "Error while creating invitation" msgstr "Erreur lors de la création de l'invitation" +#: front/src/views/content/remote/ScanForm.vue:3 +msgid "Error while fetching remote library" +msgstr "Erreur lors de la récupération de la bibliothèque distante" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "Erreur pendant l'enregistrement des paramètres" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "Erreur lors du scan de la bibliothèque" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "En erreur" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" -msgstr "Tout le monde" +#: front/src/views/content/libraries/Quota.vue:75 +msgid "Errored files" +msgstr "Fichiers erronés" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" -msgstr "Tout le monde sur cette instance" +#: front/src/views/content/remote/Card.vue:58 +msgid "Errored tracks:" +msgstr "Pistes erronées:" -#: front/src/components/library/radios/Builder.vue:57 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "Exclure" @@ -820,6 +709,7 @@ msgid "Expand" msgstr "Ouvrir" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "Date d'expiration" @@ -831,97 +721,50 @@ msgstr "Expirée" msgid "Expired/used" msgstr "Expirée/utilisée" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "Source externe. Services supportés" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "Favoris" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "Fédérer avec une nouvelle instance" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "Pistes fédérées" - -#: front/src/components/Sidebar.vue:87 -#: src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 -#: src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 -#: src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "Fédération" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "Réplication des fichiers" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "Nom du fichier" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "Envoi de fichier" - #: front/src/views/admin/library/Base.vue:5 #: src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "Fichiers" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "Filtrer le type d'album" - -#: front/src/components/library/radios/Builder.vue:56 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "Nom du filtre" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "Terminer l'import" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "Terminé" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "Tout d'abord, choisissez le mode d'import de la musique" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "Suivre" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" -msgstr "Abonnement en attente de validation" +#: front/src/views/content/remote/Card.vue:88 +msgid "Follow pending approval" +msgstr "Suivi en attente de validation" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" -msgstr "Statut de l'abonnement" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" +msgstr "Suivre les bibliothèques distantes" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "Abonnés" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "Abonné·es seulement" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "Abonné" -#: front/src/components/activity/Like.vue:12 -#: src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 +#: src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "de l'album %{ album } par %{ artist }" @@ -929,7 +772,7 @@ msgstr "de l'album %{ album } par %{ artist }" msgid "From album %{ album } by %{ artist }" msgstr "De l'album %{ album } par %{ artist }" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" msgstr "Funkwale est un projet open-source et gratuit, animé par des bénévoles. Vous pouvez nous aider à améliorer le service en remontant les problèmes que vous rencontrez, en suggérant des améliorations et en parlant du projet autour de vous !" @@ -937,7 +780,7 @@ msgstr "Funkwale est un projet open-source et gratuit, animé par des bénévole msgid "Funkwhale is compatible with other music players that support the Subsonic API." msgstr "Funkwhale est compatible avec d'autres lecteurs de musique qui supportent l'API Subsonic." -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "Funkwhale est très simple à utiliser." @@ -945,7 +788,7 @@ msgstr "Funkwhale est très simple à utiliser." msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." msgstr "Funkwhale est conçu pour faciliter l'écoute des musiques que vous aimez et découvrir de nouveaux artistes." -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." msgstr "Funkwhale est gratuit et vous donne le contrôle sur votre musique." @@ -961,16 +804,15 @@ msgstr "Obtenir une nouvelle invitation" msgid "Get me to the library" msgstr "Amenez moi à la bibliothèque" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " +#: front/src/components/Home.vue:76 +msgid "Get quality metadata about your music thanks to MusicBrainz" msgstr "" -"Obtenez des métadonnées de qualité grâce à \n" -" MusicBrainz\n" -" " +"Obtenez des métadonnées de qualité sur votre musique grâce à MusicBrainz" + +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +msgid "Get started" +msgstr "Démarrer" #: front/src/components/common/ActionTable.vue:21 #: front/src/components/common/ActionTable.vue:27 @@ -981,17 +823,9 @@ msgstr "Aller" msgid "Go to home page" msgstr "Retourner à la page d'accueil" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "Récupérer les métadonnées correspondantes" - -#: front/src/App.vue:74 +#: front/src/App.vue:73 msgid "Help us translate Funkwhale" -msgstr "Aidez nous à traduire Funkwhale" - -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "Accueil" +msgstr "Aidez-nous à traduire Funkwhale" #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" @@ -1001,150 +835,45 @@ msgstr "Heures de musique" msgid "However, accessing Funkwhale from those clients require a separate password you can set below." msgstr "Cependant, accéder à Funkwhale depuis ces clients requiert un mot de passe distinct que vous pouvez configurer ci-dessous." -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "ID" - #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." msgstr "Si l'adresse email que vous avez fournie est valide et associée à un compte utilisateur, vous allez recevoir un email contenant les instructions de réinitialisation au cours des prochaines minutes." -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "Import" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "Import #%{ id } démarré" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "Import %{ count } track" -msgstr[1] "Importer %{ count } pistes" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "Lot d'import" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "Lot d'imports #%{ id }" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "Lots d'imports" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "Lots d'imports" - #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "Date d'import" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "Page de détail de l'import" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "Importer de la musique" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "Importer de la musique" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" msgstr "Importez de la musique de différentes plate-formes, comme YouTube ou Soundcloud" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" -msgstr "Import en attente" +#: front/src/components/library/FileUpload.vue:51 +msgid "Import reference" +msgstr "Référence de l'importation" -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "Requêtes d'import" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" -msgstr "Source de l'import" - -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "Status de l'import" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "Importer cet album" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "Importer cette piste" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "Importé" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "URL importée" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "Imports" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "Dans les favoris" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "Dans la bibliothèque" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "Inactif" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "Saisir manuellement un ID MusicBrainz :" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "Saisissez l'adresse email associée à votre compte" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" msgstr[0] "Insérer depuis la queue (%{ count } piste)" msgstr[1] "Insérer depuis la queue (%{ count } pistes)" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "Informations relatives à cette instance" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "Radios de l'instance" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "Paramètres de l'instance" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "Fil de l'instance" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1160,49 +889,43 @@ msgstr "Code d'invitation (optionnel)" msgid "Invitations" msgstr "Invitations" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "Issue tracker" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "ID de la tâche" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" -msgstr "Tâches" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" +msgstr "" +"On dirait que vous n'avez pas encore de bibliothèque, il est temps d'en " +"créer une !" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" msgstr "Gardez une trace de vos chansons favorites" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." -msgstr "Gardez votre PRIVATE_TOKEN secret étant donné qu'il permet l'accès à votre compte." +#: front/src/views/content/remote/Home.vue:14 +msgid "Known libraries" +msgstr "Bibliothèques connues" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "Dernière activité" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "Dernière récupération" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "Dernière modification" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +msgid "Last update:" +msgstr "Dernière mise a jour :" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "Démarrer" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "Date de lancement" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" msgstr "Démarrer le scan" @@ -1210,25 +933,24 @@ msgstr "Démarrer le scan" msgid "Learn more about this instance" msgstr "En apprendre plus à propos de cette instance" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "Laisser vide pour obtenir un code aléatoire" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "Laisser ce champ vide si vous voulez suggérer toute la discographie." -#: front/src/views/federation/Base.vue:5 -#: src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "Bibliothèques" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "" +"Les bibliothèques vous aident à organiser et à partager votre collection de " +"musique. Vous pouvez téléverser votre bibliothèque musicale sur Funkwhale et " +"la partager avec vos amis et votre famille." + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "Bibliothèque" @@ -1236,153 +958,111 @@ msgstr "Bibliothèque" msgid "Library files" msgstr "Fichiers de la bibliothèque" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "Nom de la bibliothèque" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "Taille de la bibliothèque" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "library@demo.funkwhale.audio" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "Liens" +#: front/src/views/content/libraries/Detail.vue:21 +msgid "Loading followers..." +msgstr "Chargement des followers ..." + +#: front/src/views/content/libraries/Home.vue:3 +msgid "Loading Libraries..." +msgstr "Chargement des bibliothèques ..." + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +msgid "Loading library data..." +msgstr "Chargement des données de la bibliothèque ..." + +#: front/src/views/Notifications.vue:4 +msgid "Loading notifications..." +msgstr "Chargement des notifications ..." + +#: front/src/views/content/remote/Home.vue:3 +msgid "Loading remote libraries..." +msgstr "Chargement de bibliothèques distantes ..." + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "Chargement de la timeline ..." +#: front/src/views/content/libraries/Quota.vue:4 +msgid "Loading usage data..." +msgstr "Chargement des données d'utilisation ..." + #: front/src/components/favorites/List.vue:5 msgid "Loading your favorites..." msgstr "Chargement de vos favoris ..." -#: front/src/components/auth/Login.vue:78 -msgid "Log In" -msgstr "Connexion" - #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "Connectez vous à votre compte Funkwhale" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "Déconnexion" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "Connecté·e en tant que %{ username }" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "Connexion" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "Déconnexion" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "Répétition désactivée. Cliquez pour activer la répétition sur la piste actuelle." - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "Répétition sur la piste en cours. Cliquer pour répêter sur l'intégralité de la queue." - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "Répétition sur l'intégralité, cliquez pour désactiver la répétition." - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "Paroles" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "Gérer la bibliothèque" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "Gérer les playlists" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "Gérer les utilisateur·rices" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "Gérer vos playlists" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" -msgstr "Marquer comme fermé" +#: front/src/views/Notifications.vue:17 +msgid "Mark all as read" +msgstr "Tout marquer comme lu" -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" -msgstr "Marquer comme importé" +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" +msgstr "Mo" -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "Metadonnées" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." -msgstr "" -"Les métadonnées sont les informations relatives à la musique que vous souhaitez importer. Cela inclut notamment toutes les informations concernant les artistes, albums et pistes. Afin d'avoir des métadonnées de qualité, il est recommandé d'utiliser les métadonnées du projet \n" -" MusicBrainz\n" -" \n" -" qui est une base de données musicale fonctionnant sur le même modèle que Wikipédia." - -#: front/src/components/Sidebar.vue:48 -#: src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "Musique" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "Requête musicale" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "Couper le son" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "Mon compte" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" -msgstr "Ma playlist d'enfer" +#: front/src/views/content/libraries/Home.vue:6 +msgid "My libraries" +msgstr "Mes bibliothèques" -#: front/src/components/library/radios/Builder.vue:227 -msgid "My awesome radio" -msgstr "Ma radio d'enfer" - -#: front/src/components/library/Track.vue:64 -#: src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 +#: src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 +#: src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "ND" #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "Nom" @@ -1391,94 +1071,72 @@ msgstr "Nom" msgid "New password" msgstr "Nouveau mot de passe" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "Les nouvelles pistes seront ajoutées ici automatiquement." -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "Prochaine étape" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "Piste suivante" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "Non" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" msgstr "Pas d'addons ou d'extension à installer, il vous suffit d'une bibliothèque sur le web" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "Pas de paroles disponibles pour cette piste." -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" -msgstr "Personne à part moi" +#: front/src/components/federation/LibraryWidget.vue:6 +msgid "No matching library." +msgstr "Aucune bibliothèque correspondante." -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "Non abonné·e" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" -msgstr "Non importé" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" +msgstr "Personne ne suit cette bibliothèque" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "Pas utilisé" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +msgid "Notifications" +msgstr "Notifications" + +#: front/src/App.vue:36 msgid "Official website" msgstr "Site officiel" #: front/src/components/auth/Settings.vue:83 msgid "Old password" -msgstr "Vieux mot de passe" +msgstr "Ancien mot de passe" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "Une fois que tous vos fichiers sont chargés, cliquez sur le bouton suivant pour vérifier le statut d'import." - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 msgid "Open" msgstr "Accès libre" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "Options" -#: front/src/components/library/import/Main.vue:93 -msgid "Or" -msgstr "Ou" - #: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 -#: src/components/library/Radios.vue:33 +#: src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 -#: src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" msgstr "Ordre" #: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 -#: src/components/library/Radios.vue:41 +#: src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 -#: src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "Direction" @@ -1486,10 +1144,6 @@ msgstr "Direction" msgid "Owner" msgstr "Propriétaire" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "Page non trouvée" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "Page non trouvée !" @@ -1498,42 +1152,25 @@ msgstr "Page non trouvée !" msgid "Password" msgstr "Mot de passe" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "Mot de passe mis à jour" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "Mot de passe modifié avec succès" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "Mettre en pause" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "En attente" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" msgstr "En attente de validation" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" -msgstr "Demandes d'abonnement en attente" +#: front/src/views/content/libraries/Quota.vue:22 +msgid "Pending files" +msgstr "Fichiers en attente" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "Requêtes en attente" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "Requêtes en attente" @@ -1557,10 +1194,6 @@ msgstr "Tout lire" msgid "Play all albums" msgstr "Lire tous les albums" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "Jouer immédiatement" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "Lire ensuite" @@ -1569,14 +1202,6 @@ msgstr "Lire ensuite" msgid "Play now" msgstr "Lire maintenant" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "Jouer" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "Playlist" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1603,9 +1228,9 @@ msgstr "Playlist mise à jour" msgid "Playlist visibility" msgstr "Visibilité de la playlist" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" msgstr "Playlists" @@ -1625,43 +1250,45 @@ msgstr "Merci de vérifier que votre nom d'utilisateur et mot de passe sont corr msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." msgstr "PNG, GIF ou JPG. 2Mo au plus. L'image sera réduite à 400×400 pixels." -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "Étape précédente" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" -msgstr "Piste précédente" +#: front/src/components/library/FileUpload.vue:58 +#, fuzzy +msgid "Proceed" +msgstr "Poursuivre" #: front/src/views/auth/EmailConfirm.vue:26 #: front/src/views/auth/PasswordResetConfirm.vue:31 msgid "Proceed to login" msgstr "Poursuivre vers la page de connexion" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" -msgstr "Date de sortie" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" +msgstr "En traitement" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" -msgstr "Template de recherche" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "Nettoyer" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "Nettoyer les fichiers erronés ?" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "Nettoyer les fichiers en attente ?" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" +msgstr "Nettoyer les fichiers oubliés ?" #: front/src/components/Sidebar.vue:20 msgid "Queue" msgstr "Queue" -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "La queue a été mélangée !" - -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "Radio" - -#: front/src/components/library/radios/Builder.vue:226 -msgid "Radio Builder" -msgstr "Éditeur de radio" - #: front/src/components/library/radios/Builder.vue:15 msgid "Radio created" msgstr "Radio créée" @@ -1672,10 +1299,10 @@ msgstr "Nom de la radio" #: front/src/components/library/radios/Builder.vue:12 msgid "Radio updated" -msgstr "Radio à jour" +msgstr "Radio mise à jour" #: front/src/components/library/Library.vue:10 -#: src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 msgid "Radios" msgstr "Radios" @@ -1695,19 +1322,11 @@ msgstr "Récemment ajouté aux favoris" msgid "Recently listened" msgstr "Écoutées récemment" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "ID MusicBrainz de l'enregistrement" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" msgstr "Rafraîchir" -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "Refusé" - #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" msgstr "Inscrit·e depuis le %{ date }" @@ -1720,6 +1339,22 @@ msgstr "Les inscriptions sont fermées sur cette instance, vous aurez besoin d'u msgid "regular user" msgstr "utilisateur·rice standard·e" +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "Rejeter" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "Rejeté" + +#: front/src/views/content/remote/Home.vue:6 +msgid "Remote libraries" +msgstr "Bibliothèques distantes" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." +msgstr "" + #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" msgstr "Retirer" @@ -1728,10 +1363,6 @@ msgstr "Retirer" msgid "Remove avatar" msgstr "Supprimer mon avatar" -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "Retirer des favoris" - #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" @@ -1745,109 +1376,60 @@ msgstr "Demander un nouveau mot de passe pour l'API Subsonic ?" msgid "Request a password" msgstr "Demander un mot de passe" -#: front/src/App.vue:35 -msgid "Request music" -msgstr "Demandes de musique" - -#: front/src/views/library/MusicRequest.vue:4 -#: src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "Demandez de la musique" - #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" msgstr "Requête envoyée" -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "Relancer les tâches échouées" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "Relancer la tâche" - #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" msgstr "Réinitialiser votre mot de passe" -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "Résultat %{ current }/%{ total }" - #: front/src/components/favorites/List.vue:38 #: src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 -#: src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" msgstr "Résultats par page" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:29 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" msgstr "Enregistrer" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" -msgstr "Scan démarré !" +#: front/src/views/content/remote/Card.vue:31 +msgid "Scan pending" +msgstr "En attente d'analyse" + +#: front/src/views/content/remote/Card.vue:43 +msgid "Scanned successfully" +msgstr "Scanné avec succès" + +#: front/src/views/content/remote/Card.vue:47 +msgid "Scanned with errors" +msgstr "Scanné avec des erreurs" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" +msgstr "Balayage... (%{ progress }%)" -#: front/src/components/federation/LibraryTrackTable.vue:5 #: front/src/components/library/Artists.vue:10 #: src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:5 #: src/views/playlists/List.vue:13 msgid "Search" msgstr "Rechercher" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "Rechercher une ressource que vous voulez importer :" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "Rechercher par artiste, nom d'utilisateur·rice, commentaire..." - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "Rechercher par source..." - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "Rechercher par utilisateur·rice, source..." - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "Rechercher par titre, artiste, domaine..." - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "Rechercher par nom d'utilisateur·rice, domaine..." - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "Rechercher par nom d'utilisateur·rice, email, code..." - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "Rechercher par nom d'utilisateur·rice, email, nom..." - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." -msgstr "Rechercher des artistes, des albums, des pistes" +#: front/src/views/content/remote/ScanForm.vue:9 +msgid "Search a remote library" +msgstr "Rechercher dans une bibliothèque distante" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" msgstr "Rechercher de la musique" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" msgstr "Rechercher sur lyrics.wikia.com" @@ -1857,43 +1439,27 @@ msgstr "Rechercher sur lyrics.wikia.com" msgid "Search on Wikipedia" msgstr "Rechercher sur Wikipédia" -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "Recherche" - #: front/src/views/admin/Settings.vue:15 msgid "Sections" msgstr "Sections" -#: front/src/components/library/radios/Builder.vue:41 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" msgstr "Sélectionner un filtre" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" msgstr[0] "Selectionner l'ensemble des %{ total } élément" msgstr[1] "Selectionner l'ensemble des %{ total } éléments" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "Sélectionner les fichiers à envoyer..." - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" msgstr "Sélectionner seulement la page actuelle" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "Sélectionner les sources ou fichiers à importer" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "Envoyer une demande d'abonnement" - -#: front/src/components/Sidebar.vue:97 -#: src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" msgstr "Paramètres" @@ -1905,69 +1471,65 @@ msgstr "Paramètres mis à jour" msgid "Settings updated successfully." msgstr "Paramètres modifiés avec succès." -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "Paramètres..." - #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" msgstr "Lien de partage" +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." +msgstr "Partage ce lien avec les autres utilisateurs afin qu'ils aient accès a cette bibliothèque." + +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +msgid "Sharing link" +msgstr "Lien de partage" + +#: front/src/components/audio/album/Card.vue:40 +#, fuzzy +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "%{ count } piste" +msgstr[1] "%{ count } pistes" + #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" msgstr[0] "Montrer 1 album supplémentaire" msgstr[1] "Montrer %{ count } albums supplémentaires" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "Afficher 1 piste de plus" -msgstr[1] "Afficher %{ count } pistes de plus" +#: front/src/views/Notifications.vue:10 +msgid "Show read notifications" +msgstr "Afficher les notifications lues" -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" -msgstr "Afficher/masquer le mot de passe" - -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" msgstr "Affichage des résultats %{ start }-%{ end } sur %{ total }" -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "Mélanger votre queue" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "Inscription" - #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" msgstr "Inscription" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "Copiez le texte ci-dessous dans un terminal pour lancer le téléchargement." - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 +#: src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" msgstr "Taille" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 msgid "Skipped" msgstr "Ignoré" +#: front/src/views/content/libraries/Quota.vue:49 +msgid "Skipped files" +msgstr "Fichiers ignorés" + #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "Quelque chose manque dans la bibliothèque ? Dîtes nous ce que vous voulez écouter !" @@ -1980,21 +1542,15 @@ msgstr "Désolé, nous n'avons trouvé aucun album correspondant à votre recher msgid "Sorry, we did not found any artist matching your query" msgstr "Désolé, nous n'avons trouver aucun·a artiste correspondant à votre recherche" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "Source" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "Code source" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "Code source (%{version})" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" msgstr "Membre de l'équipe" @@ -2003,26 +1559,11 @@ msgstr "Membre de l'équipe" msgid "Start" msgstr "Démarrer" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "Démarrer l'envoi" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "Statistiques" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "Statut" @@ -2030,15 +1571,11 @@ msgstr "Statut" msgid "Stop" msgstr "Arrêter" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" msgstr "Arrêter la radio" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "Arrêter l'envoi" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" msgstr "Valider" @@ -2046,46 +1583,43 @@ msgstr "Valider" msgid "Submit another request" msgstr "Envoyer une nouvelle requête" -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "Proposé par" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "Subsonic" - #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" msgstr "Mot de passe de l'API Subsonic" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" -msgstr "Succès" - -#: front/src/App.vue:11 +#: front/src/App.vue:13 msgid "Suggested choices" msgstr "Suggestions" +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" +msgstr "" + #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "Synchronisation des changements avec le serveur..." +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "Texte copier dans le presse-papier!" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." msgstr "C'est simple : nous adorions Grooveshark et nous voulions construire quelque chose d'encore mieux." -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "Les Beatles, Mickael Jackson..." - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." msgstr "Le logo de Funkwhale a été généreusement dessiné et fourni par Francis Gading." -#: front/src/components/Home.vue:124 +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." +msgstr "Cette bibliothèque et toutes ces pistes seront supprimée. Cette action est irréversible." + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "" + +#: front/src/components/Home.vue:121 msgid "The plaform is free and open-source, you can install it and modify it without worries" msgstr "La plateforme est gratuite et open-source, vous pouvez l'installer et la modifier sans restrictions" @@ -2093,33 +1627,37 @@ msgstr "La plateforme est gratuite et open-source, vous pouvez l'installer et la msgid "The Subsonic API is not available on this Funkwhale instance." msgstr "L'API Subsonic n'est pas disponible sur cette instance Funkwhale." -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" -msgstr "L'Album Blanc, Thriller..." +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" +msgstr "" -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." -msgstr "Il n'est pas possible pour le moment de télécharger un ensemble de pistes sous forme d'archive. Cependant, vous pouvez utiliser un outil en ligne de commande tel que cURL pour télécharger facilement une liste de pistes." +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" +msgstr "" -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." -msgstr "Cet import sera associé à la requête ci dessous. Une fois l'import terminé, la requête sera marquée comme complétée." +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" +msgstr "" -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" -msgstr "Ceci indique si la bibliothèque distance vous à accordé l'accès" +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." +msgstr "" #: front/src/components/auth/Profile.vue:16 msgid "This is you!" msgstr "C'est vous !" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." msgstr "Cette opération peut affecter de nombreux éléments, merci de vérifier que c'est bien ce que vous souhaitez." -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" -msgstr "Cette piste n'est pas importée et ne peut pas être jouée" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "" + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" +msgstr "" #: front/src/views/playlists/Detail.vue:37 msgid "This will completely delete this playlist and cannot be undone." @@ -2133,9 +1671,165 @@ msgstr "Cela supprimera définitivement cette radio et ne pourra pas être annul msgid "This will completely disable access to the Subsonic API using from account." msgstr "Cela désactivera complétement l'accès à l'API Subsonic depuis votre compte." -#: front/src/App.vue:137 +#: front/src/App.vue:162 src/components/About.vue:55 +#: src/components/Home.vue:154 front/src/components/PageNotFound.vue:33 +#: src/components/Sidebar.vue:203 front/src/components/Sidebar.vue:204 +#: src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 +#: src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 +#: src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 +#: src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 +#: src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 +#: src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 +#: src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 +#: src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 +#: src/components/audio/SearchBar.vue:27 front/src/components/auth/Login.vue:77 +#: src/components/auth/Login.vue:78 front/src/components/auth/Logout.vue:20 +#: src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 +#: src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 +#: src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 +#: src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 +#: src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 +#: src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 +#: src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 +#: src/views/auth/EmailConfirm.vue:51 front/src/views/auth/PasswordReset.vue:52 +#: src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +#: src/views/content/Base.vue:19 front/src/views/content/Home.vue:35 +#: src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 +#: src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 +#: src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 +#: src/views/instance/Timeline.vue:57 front/src/views/playlists/Detail.vue:90 +#: src/views/playlists/List.vue:104 front/src/views/playlists/List.vue:105 +#: src/views/radios/Detail.vue:80 msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "Cela va effacer vos données locales et vous déconnecter, voulez-vous continuer ?" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "" +"Cela effacera vos données locales et vous déconnectera. Voulez-vous " +"continuer ?" +msgstr[1] "%{ count } les pistes ont été ajoutées à votre file d'attente" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." @@ -2145,19 +1839,25 @@ msgstr "Cela vous déconnectera sur l'ensemble de vos appareils utilisant ce mot msgid "This will remove all tracks from this playlist and cannot be undone." msgstr "Cela supprimera toutes les pistes de la playlist et ne pourra pas être annulé." +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + #: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 #: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 msgid "Title" msgstr "Titre" -#: front/src/components/audio/SearchBar.vue:27 -#: src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" -msgstr "Piste" - #: front/src/components/library/Track.vue:53 msgid "Track information" msgstr "Information de la piste" @@ -2172,15 +1872,11 @@ msgstr "pistes" #: front/src/components/library/Album.vue:43 #: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 -#: front/src/views/radios/Detail.vue:34 +#: src/views/content/Base.vue:8 front/src/views/content/libraries/Detail.vue:8 +#: src/views/playlists/Detail.vue:50 front/src/views/radios/Detail.vue:34 msgid "Tracks" msgstr "Pistes" -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "Pistes disponibles dans cette bibliothèque" - #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" msgstr "Pistes par cet⋅te artiste" @@ -2193,34 +1889,36 @@ msgstr "Pistes en favoris" msgid "tracks listened" msgstr "pistes écoutées" -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "Déclencher un scan" - +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" msgstr "Type" +#: front/src/views/content/remote/Card.vue:100 +#: src/views/content/remote/Card.vue:105 +msgid "Unfollow" +msgstr "Se désabonner" + +#: front/src/views/content/remote/Card.vue:101 +msgid "Unfollow this library?" +msgstr "Se désabonner de cette bibliothèque ?" + #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." msgstr "Malheureusement, les gestionnaires de cette instance n'ont pas encore pris le temps de compléter cette page." -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "Inconnu" - #: front/src/components/Home.vue:37 msgid "Unlimited music" msgstr "Musique illimitée" -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "Réactiver le son" - #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" msgstr "Mettre à jour l'avatar" +#: front/src/views/content/libraries/Form.vue:25 +msgid "Update library" +msgstr "Mettre à jour la bibliothèque" + #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" msgstr "Mettre à jour la playlist" @@ -2233,48 +1931,62 @@ msgstr "Mettre à jour les paramètres" msgid "Update your password" msgstr "Mettre à jour votre mot de passe" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" msgstr "Envoi" #: front/src/components/auth/Settings.vue:45 msgid "Upload a new avatar" -msgstr "Envoyer un nouvel avatar" +msgstr "Téléverser un nouvel avatar" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" -msgstr "Fichiers envoyés ou source externe" +#: front/src/views/content/Home.vue:6 +msgid "Upload audio content" +msgstr "Téléverser du contenu audio" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/views/content/libraries/FilesTable.vue:54 +msgid "Upload date" +msgstr "Date d'envoi" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:31 +msgid "Upload new tracks" +msgstr "Téléverser de nouveaux morceaux" + +#: front/src/views/admin/users/UsersDetail.vue:82 +msgid "Upload quota" +msgstr "Quota d'envoi" + +#: front/src/components/library/FileUpload.vue:99 +msgid "Uploaded" +msgstr "Téléversés" + +#: front/src/components/library/FileUpload.vue:5 +msgid "Uploading" +msgstr "Envoi en cours" + +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "Envoi en cours..." -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" msgstr "Utiliser une autre instance" -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "Utilisez ce champ pour ajouter des détails à votre demande, si nécessaire" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "Utilisez ce réglage pour activer/désactiver la fédération avec cette bibliothèque" - #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." msgstr "Utilisez ce formulaire pour demander à réinitialiser votre mot de passe. Vous recevrez un email à l'adresse indiquée contenant les instructions de réinitialisation." -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "Utilisez ce formulaire pour scanner le catalogue d'une instance et mettre en place la fédération." - #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" msgstr "Utilisé" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" msgstr "Utilisateur·rice" @@ -2282,12 +1994,19 @@ msgstr "Utilisateur·rice" msgid "User activity" msgstr "Activité des utilisateur·ice·s" +#: front/src/components/library/Album.vue:49 +#: src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +msgid "User libraries" +msgstr "Bibliothèques utilisateur" + #: front/src/components/library/Radios.vue:20 msgid "User radios" msgstr "Radios des utilisateur·ice·s" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" msgstr "Nom d'utilisateur" @@ -2299,21 +2018,30 @@ msgstr "Nom d'utilisateur ou email" msgid "users" msgstr "utilisateur·rice·s" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 +#: src/views/admin/users/UsersList.vue:21 front/src/views/admin/Settings.vue:81 msgid "Users" msgstr "Utilisateur·ice·s" +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +msgid "View files" +msgstr "Afficher les fichiers" + #: front/src/components/library/Album.vue:37 #: src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" msgstr "Voir sur MusicBrainz" +#: front/src/views/content/libraries/Form.vue:18 +msgid "Visibility" +msgstr "Visibilité" + #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" msgstr "Nous ne pouvons pas ajouter cette piste à une playlist" @@ -2338,13 +2066,21 @@ msgstr "Nous ne pouvons pas enregistrer votre avatar" msgid "We cannot save your settings" msgstr "Nous ne pouvons pas enregistrer vos paramètres" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" msgstr "Nous ne vous pistons pas et ne vous exposons pas à des publicités" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." -msgstr "Nous recommandons d'utiliser le logiciel Picard pour cela." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:40 +msgid "we recommend using Picard for that purpose" +msgstr "nous vous recommandons d'utiliser le logiciel Picard pour cela" #: front/src/components/Home.vue:7 msgid "We think listening to music should be simple." @@ -2358,27 +2094,15 @@ msgstr "Désolé, la page demandée n’existe pas :" msgid "We've received your request, you'll get some groove soon ;)" msgstr "Nous avons bien reçu votre requête, vous aurez bientôt de nos nouvelles ;)" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "Bienvenue" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" msgstr "Bienvenue sur Funkwhale" -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "Qu'est-ce que les métadonnées ?" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "Si ce réglage est activé, les nouvelles pistes ajoutées dans cette bibliothèque seront automatiquement importées" - #: front/src/components/Home.vue:24 msgid "Why funkwhale?" msgstr "Pourquoi Funkwhale ?" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" msgstr "Oui" @@ -2386,15 +2110,19 @@ msgstr "Oui" msgid "Yes, log me out!" msgstr "Oui, déconnectez-moi !" +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" +msgstr "" + #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" msgstr "Vous êtes actuellement connecté·e en tant que %{ username }" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." -msgstr "Vous pouvez également sauter cette étape et entrer les métadonnées manuellement." +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." +msgstr "" -#: front/src/components/Home.vue:136 +#: front/src/components/Home.vue:133 msgid "You can invite friends and family to your instance so they can enjoy your music" msgstr "Vous pouvez inviter vos ami·es et votre famille sur votre instance pour qu'ils·elles puissent profiter de votre musique" @@ -2406,14 +2134,18 @@ msgstr "Vous pouvez utiliser cette interface pour réaliser votre propre radio p msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." msgstr "Vous pouvez les utiliser pour profiter de vos playlists et de votre musique en mode hors-ligne sur votre smatphone ou tablette, par exemple." -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" msgstr "Vous écoutez une radio" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" msgstr "Vous devez choisir une instance pour continuer" +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." +msgstr "" + #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" msgstr "Vous allez être déconnecté⋅e de cette session et vous allez devoir vous connecter avec votre nouveau mot de passe" @@ -2422,22 +2154,18 @@ msgstr "Vous allez être déconnecté⋅e de cette session et vous allez devoir msgid "You will have to update your password on your clients that use this password." msgstr "Vous devrez mettre à jour votre mot de passe sur l'ensemble des clients utilisant ce mot de passe." -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "Vous allez importer :" - #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." msgstr "Votre adresse email a été confirmée, vous pouvez maintenant utiliser le service sans limitations." -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" -msgstr "Vos favoris" - -#: front/src/components/Home.vue:117 +#: front/src/components/Home.vue:114 msgid "Your music, your way" msgstr "Votre musique, à votre façon" +#: front/src/views/Notifications.vue:7 +msgid "Your notifications" +msgstr "Vos notifications" + #: front/src/views/auth/PasswordResetConfirm.vue:29 msgid "Your password has been updated successfully." msgstr "Votre mot de passe a été mis à jour avec succès." @@ -2446,8 +2174,404 @@ msgstr "Votre mot de passe a été mis à jour avec succès." msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" msgstr "Votre mot de passe Subsonic sera remplacé par un nouveau mot de passe aléatoire, ce qui vous déconnectera de tous les appareils utilisant l'ancien mot de passe" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +msgid "Activity visibility" +msgstr "Visibilité de l'activité" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 +#: src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "Personne à part moi" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 +#: src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "Tout le monde sur cette instance" + +#: front/src/components/mixins/Translations.vue:18 +msgid "Accessed date" +msgstr "Date d'accès" + +#: front/src/components/mixins/Translations.vue:19 +msgid "Modification date" +msgstr "Date de modification" + +#: front/src/components/mixins/Translations.vue:20 +msgid "Imported date" +msgstr "Date d'import" + +#: front/src/components/mixins/Translations.vue:22 +msgid "Track name" +msgstr "Nom de la piste" + +#: front/src/components/mixins/Translations.vue:23 +msgid "Album name" +msgstr "Nom de l'album" + +#: front/src/components/mixins/Translations.vue:30 +msgid "Sign-up date" +msgstr "Date d'inscription" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "Copier les pistes de la queue dans la playlist" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "Ajouter à cette playlist" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "Ma playlist d'enfer" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "Tout le monde" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "Inscription" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "Saisissez votre code d'invitation (insensible à la casse)" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "Saisissez votre nom d'utilisateur·rice" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "Saisissez votre email" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "Mot de passe mis à jour" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "Accès désactivé" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "Saisissez votre nom d'utilisateur·rice ou email" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "Connexion" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "Profil de %{ username }" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "Déconnexion" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "Paramètres du compte" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "Retirer des favoris" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "Vos favoris" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "Saisissez un nom de radio..." + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "Éditeur de radio" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "Ma radio d'enfer" + +#: front/src/components/library/radios/Builder.vue:236 +msgid "My awesome description" +msgstr "Ma description géniale" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "" + +#: front/src/components/library/FileUpload.vue:240 +msgid "A network error occured while uploading this file" +msgstr "Une erreur réseau s'est produite lors du téléversement de ce fichier" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "Saisissez un nom d'artiste..." + +#: front/src/components/library/Track.vue:195 +#: src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "Piste" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "Accueil" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "Afficher/masquer le mot de passe" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "Les Beatles, Mickael Jackson..." + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "L'Album Blanc, Thriller..." + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "Utilisez ce champ pour ajouter des détails à votre demande, si nécessaire" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" msgstr[0] "%{ count } piste a été ajouté à votre queue" msgstr[1] "%{ count } pistes ont été ajoutées à votre queue" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "Artiste, album, piste..." + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "Rechercher des artistes, des albums, des pistes" + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "La queue a été mélangée !" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "Piste précédente" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "Jouer" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "Mettre en pause" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "Piste suivante" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "Réactiver le son" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "Couper le son" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "Répétition désactivée. Cliquez pour activer la répétition sur la piste actuelle." + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "Répétition sur la piste en cours. Cliquer pour répêter sur l'intégralité de la queue." + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "Répétition sur l'intégralité, cliquez pour désactiver la répétition." + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "Mélanger votre queue" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "Vider la queue" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "Requêtes en attente" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "Demandes d'abonnement en attente" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "Saisissez votre recherche..." + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "Rechercher par titre, artiste, domaine..." + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "Laisser vide pour obtenir un code aléatoire" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "Rechercher par nom d'utilisateur·rice, email, code..." + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "Rechercher par nom d'utilisateur·rice, email, nom..." + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "Fédération" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "" + +#: front/src/views/content/remote/ScanForm.vue:48 +msgid "Enter a library url" +msgstr "Entrez une URL de bibliothèque" + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:226 +msgid "Search by title, artist, album..." +msgstr "Rechercher par titre, artiste, album ..." + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:242 +msgid "Import went on successfully" +msgstr "L'importation s'est bien déroulée" + +#: front/src/views/content/libraries/FilesTable.vue:259 +msgid "Relaunch import" +msgstr "Relancer l'importation" + +#: front/src/views/content/libraries/Card.vue:58 +msgid "Visibility: nobody except me" +msgstr "Visibilité: personne sauf moi" + +#: front/src/views/content/libraries/Card.vue:59 +msgid "Visibility: everyone on this instance" +msgstr "Visibilité: tout le monde sur cette instance" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:61 +msgid "Total size of the files in this library" +msgstr "Taille totale des fichiers de cette bibliothèque" + +#: front/src/views/content/libraries/Form.vue:70 +msgid "My awesome library" +msgstr "Ma bibliothèque d'enfer" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:74 +msgid "Everyone, including other instances" +msgstr "Tout le monde, y compris d'autres instances" + +#: front/src/views/content/libraries/Form.vue:106 +msgid "Library updated" +msgstr "Bibliothèque actualisée" + +#: front/src/views/content/libraries/Form.vue:109 +msgid "Library created" +msgstr "Bibliothèque créée" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "Radio" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "Fil de l'instance" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "Playlist" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "Saisissez un nom de playlist..." + +#: front/src/views/admin/library/Base.vue:16 +msgid "Manage library" +msgstr "Gérer la bibliothèque" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "Détermine si le compte utilisateur est actif ou non. Les comptes inactifs ne peuvent pas se connecter ou utiliser le service." + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "" + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "Gérer les utilisateur·rices" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "Paramètres de l'instance" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "Informations relatives à cette instance" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "Subsonic" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "Statistiques" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "Rapports d'erreur" diff --git a/front/locales/gl/LC_MESSAGES/app.po b/front/locales/gl/LC_MESSAGES/app.po new file mode 100644 index 000000000..01424080a --- /dev/null +++ b/front/locales/gl/LC_MESSAGES/app.po @@ -0,0 +1,2495 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the front package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: front 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-07-22 14:12+0200\n" +"PO-Revision-Date: 2018-08-24 10:28+0000\n" +"Last-Translator: Xosé M \n" +"Language-Team: none\n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 2.20\n" + +#: front/src/components/playlists/PlaylistModal.vue:9 +msgid "\"%{ title }\", by %{ artist }" +msgstr "\"%{ title }\", de %{ artist }" + +#: front/src/components/Sidebar.vue:24 +msgid "(%{ index } of %{ length })" +msgstr "(%{ index } de %{ length })" + +#: front/src/components/Sidebar.vue:22 +msgid "(empty)" +msgstr "(baldeiro)" + +#: front/src/components/common/ActionTable.vue:43 +#: front/src/components/common/ActionTable.vue:51 +msgid "%{ count } on %{ total } selected" +msgid_plural "%{ count } on %{ total } selected" +msgstr[0] "%{ count } de %{ total } seleccionado" +msgstr[1] "%{ count } de %{ total } seleccionados" + +#: front/src/components/Sidebar.vue:116 src/views/federation/LibraryDetail.vue:87 +msgid "%{ count } track" +msgid_plural "%{ count } tracks" +msgstr[0] "%{ count } canción" +msgstr[1] "%{ count } cancións" + +#: front/src/components/library/Artist.vue:13 +msgid "%{ count } track in %{ albumsCount } albums" +msgid_plural "%{ count } tracks in %{ albumsCount } albums" +msgstr[0] "%{ count } canción en %{ albumsCount } álbumes" +msgstr[1] "%{ count } cancións en %{ albumsCount } álbumes" + +#: front/src/components/library/radios/Builder.vue:76 +msgid "%{ count } track matching combined filters" +msgid_plural "%{ count } tracks matching combined filters" +msgstr[0] "%{ count } canción coicidente cos filtros combinados" +msgstr[1] "%{ count } cancións coincidentes cos filtros combinados" + +#: front/src/components/playlists/Card.vue:18 +msgid "%{ count} track" +msgid_plural "%{ count } tracks" +msgstr[0] "%{ count} canción" +msgstr[1] "%{ count } cancións" + +#: front/src/components/common/Duration.vue:2 +msgid "%{ hours } h %{ minutes } min" +msgstr "%{ hours } h %{ minutes } min" + +#: front/src/components/common/Duration.vue:5 +msgid "%{ minutes } min" +msgstr "%{ minutes } min" + +#: front/src/components/activity/Like.vue:7 +msgid "%{ user } favorited a track" +msgstr "%{ user } marcou favorita unha canción" + +#: front/src/components/activity/Listen.vue:7 +msgid "%{ user } listened to a track" +msgstr "%{ user } escoitou unha canción" + +#: front/src/components/auth/Profile.vue:49 +msgid "%{ username }'s profile" +msgstr "Perfil de %{ username }" + +#: front/src/components/audio/artist/Card.vue:41 +msgid "1 album" +msgid_plural "%{ count } albums" +msgstr[0] "1 álbume" +msgstr[1] "%{ count } álbumes" + +#: front/src/components/favorites/List.vue:10 +msgid "1 favorite" +msgid_plural "%{ count } favorites" +msgstr[0] "1 favorita" +msgstr[1] "%{ count } favoritas" + +#: front/src/components/audio/album/Card.vue:54 +#: front/src/components/federation/LibraryCard.vue:25 +msgid "1 track" +msgid_plural "%{ count } tracks" +msgstr[0] "1 canción" +msgstr[1] "%{ count } cancións" + +#: front/src/components/About.vue:5 +msgid "About %{ instance }" +msgstr "Acerca de %{ instance }" + +#: front/src/App.vue:54 +msgid "About Funkwhale" +msgstr "Acerca de Funkwhale" + +#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +msgid "About this instance" +msgstr "Acerca de esta instancia" + +#: front/src/components/manage/library/RequestsTable.vue:28 +#: front/src/components/manage/library/RequestsTable.vue:62 +msgid "Accepted" +msgstr "Aceptado" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "Acceso desactivado" + +#: front/src/components/Home.vue:109 +msgid "Access your music from a clean interface that focus on what really matters" +msgstr "" +"Acceda a súa música desde unha interface clara que se centra no realmente " +"importante" + +#: front/src/views/admin/users/UsersDetail.vue:54 +msgid "Account active" +msgstr "Conta activa" + +#: front/src/components/auth/Settings.vue:5 +msgid "Account settings" +msgstr "Axustes da conta" + +#: front/src/components/auth/Settings.vue:257 +msgid "Account Settings" +msgstr "Axustes da conta" + +#: front/src/components/manage/users/UsersTable.vue:39 +msgid "Account status" +msgstr "Estado da conta" + +#: front/src/views/auth/PasswordReset.vue:14 +msgid "Account's email" +msgstr "Correo-e da conta" + +#: front/src/components/common/ActionTable.vue:82 +msgid "Action %{ action } was launched successfully on %{ count } element" +msgid_plural "Action %{ action } was launched successfully on %{ count } elements" +msgstr[0] "" +"A acción %{ action } foi lanzada correctamente sobre %{ count } elemento" +msgstr[1] "" +"A accións %{ action } foi lanzada correctamente sobre %{ count } elementos" + +#: front/src/components/common/ActionTable.vue:8 +#: front/src/components/federation/LibraryFollowTable.vue:24 +#: front/src/components/library/radios/Builder.vue:60 +#: front/src/components/manage/library/RequestsTable.vue:54 +msgid "Actions" +msgstr "Accións" + +#: front/src/components/manage/users/UsersTable.vue:53 +msgid "Active" +msgstr "Activo" + +#: front/src/components/Sidebar.vue:60 +msgid "Activity" +msgstr "Actividade" + +#: front/src/components/federation/LibraryFollowTable.vue:21 +msgid "Actor" +msgstr "Actor" + +#: front/src/views/federation/LibraryList.vue:8 +msgid "Add a new library" +msgstr "Engadir unha nova biblioteca" + +#: front/src/components/library/radios/Builder.vue:46 +msgid "Add filter" +msgstr "Engadir filtro" + +#: front/src/components/library/radios/Builder.vue:36 +msgid "Add filters to customize your radio" +msgstr "Engada filtros para personalizar a súa radio" + +#: front/src/components/audio/PlayButton.vue:53 +msgid "Add to current queue" +msgstr "Engadir a cola actual" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:4 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +msgid "Add to favorites" +msgstr "Engadir a favoritas" + +#: front/src/components/playlists/TrackPlaylistIcon.vue:6 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +msgid "Add to playlist..." +msgstr "Engadir a lista de reprodución..." + +#: front/src/components/audio/PlayButton.vue:14 +msgid "Add to queue" +msgstr "Engadir a cola" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "Engadir a esta lista de reprodución" + +#: front/src/components/playlists/PlaylistModal.vue:54 +msgid "Add track" +msgstr "Engadir canción" + +#: front/src/components/manage/users/UsersTable.vue:69 +msgid "Admin" +msgstr "Admin" + +#: front/src/components/Sidebar.vue:64 +msgid "Administration" +msgstr "Administración" + +#: front/src/components/audio/SearchBar.vue:26 src/components/audio/track/Table.vue:8 +#: front/src/components/federation/LibraryTrackTable.vue:49 +#: front/src/components/library/Album.vue:91 +#: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/components/metadata/Search.vue:134 +msgid "Album" +msgstr "Álbume" + +#: front/src/components/library/import/ReleaseImport.vue:3 +msgid "Album %{ title } (%{ count } track) by %{ artist }" +msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" +msgstr[0] "Álbume %{ title } (%{ count } canción) de %{ artist }" +msgstr[1] "Álbume %{ title } (%{ count } cancións) de %{ artist }" + +#: front/src/components/library/Album.vue:12 +msgid "Album containing %{ count } track, by %{ artist }" +msgid_plural "Album containing %{ count } tracks, by %{ artist }" +msgstr[0] "Álbume que contén %{ count } canción, de %{ artist }" +msgstr[1] "Álbume que contén %{ count } cancións, de %{ artist }" + +#: front/src/components/library/Track.vue:20 +msgid "Album page" +msgstr "Páxina do álbume" + +#: front/src/components/audio/Search.vue:19 src/components/instance/Stats.vue:48 +#: front/src/components/manage/library/RequestsTable.vue:50 +#: front/src/components/requests/Form.vue:9 +msgid "Albums" +msgstr "Álbumes" + +#: front/src/components/library/Artist.vue:44 +msgid "Albums by this artist" +msgstr "Álbumes de este artista" + +#: front/src/components/manage/library/RequestsTable.vue:26 +#: front/src/components/manage/users/InvitationsTable.vue:19 +msgid "All" +msgstr "Todos" + +#: front/src/components/playlists/Editor.vue:13 +msgid "An error occured while saving your changes" +msgstr "Algo fallou ao gardar os cambios" + +#: front/src/components/auth/Login.vue:10 +msgid "An unknown error happend, this can mean the server is down or cannot be reached" +msgstr "" +"Aconteceu un fallo descoñecido, esto pode significar que o servidor está " +"caído ou non pode ser alcanzado" + +#: front/src/components/federation/LibraryTrackTable.vue:11 +#: front/src/components/library/import/BatchDetail.vue:68 +#: front/src/components/library/import/BatchList.vue:13 +#: front/src/components/library/import/BatchList.vue:22 +msgid "Any" +msgstr "Calquera" + +#: front/src/components/library/import/BatchList.vue:24 +msgid "API" +msgstr "API" + +#: front/src/components/federation/LibraryFollowTable.vue:68 +#: front/src/components/federation/LibraryFollowTable.vue:78 +msgid "Approve" +msgstr "Aprovar" + +#: front/src/components/federation/LibraryFollowTable.vue:70 +msgid "Approve access?" +msgstr "Permitir acceso?" + +#: front/src/components/federation/LibraryFollowTable.vue:38 +msgid "Approved" +msgstr "Permitido" + +#: front/src/components/auth/Logout.vue:5 +msgid "Are you sure you want to log out?" +msgstr "Está segura de que quere desconectar?" + +#: front/src/components/audio/SearchBar.vue:25 src/components/audio/track/Table.vue:7 +#: front/src/components/federation/LibraryTrackTable.vue:48 +#: front/src/components/library/Artist.vue:119 +#: front/src/components/manage/library/FilesTable.vue:38 +#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/components/metadata/Search.vue:130 +msgid "Artist" +msgstr "Artista" + +#: front/src/components/requests/Form.vue:5 +msgid "Artist name" +msgstr "Nome do artista" + +#: front/src/components/library/Album.vue:22 src/components/library/Track.vue:23 +msgid "Artist page" +msgstr "Páxina do artista" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "Artista, álbume, canción..." + +#: front/src/components/audio/Search.vue:10 src/components/instance/Stats.vue:42 +#: front/src/components/library/Artists.vue:119 src/components/library/Library.vue:7 +msgid "Artists" +msgstr "Artistas" + +#: front/src/components/favorites/List.vue:33 +#: front/src/components/federation/LibraryTrackTable.vue:28 +#: front/src/components/library/Artists.vue:25 src/components/library/Radios.vue:44 +#: front/src/components/manage/library/FilesTable.vue:19 +#: front/src/components/manage/library/RequestsTable.vue:19 +#: front/src/components/manage/users/UsersTable.vue:19 +#: front/src/views/federation/LibraryList.vue:28 src/views/playlists/List.vue:27 +msgid "Ascending" +msgstr "Ascendente" + +#: front/src/views/auth/PasswordReset.vue:27 +msgid "Ask for a password reset" +msgstr "Solicitar restablecer o contrasinal" + +#: front/src/views/federation/LibraryDetail.vue:56 +msgid "Auto importing" +msgstr "Importar automáticamente" + +#: front/src/components/playlists/PlaylistModal.vue:26 +msgid "Available playlists" +msgstr "Listas de reprodución dispoñibles" + +#: front/src/components/auth/Settings.vue:34 +msgid "Avatar" +msgstr "Avatar" + +#: front/src/views/auth/EmailConfirm.vue:17 src/views/auth/PasswordReset.vue:24 +#: front/src/views/auth/PasswordResetConfirm.vue:18 +msgid "Back to login" +msgstr "Voltar a conectar" + +#: front/src/views/federation/LibraryFollowersList.vue:5 +msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." +msgstr "" +"Teña coidado cando acepte solicitudes de seguimento, xa que esto dará acceso " +"a seguidora a toda a súa biblioteca." + +#: front/src/components/library/Track.vue:80 +#: front/src/components/manage/library/FilesTable.vue:42 +msgid "Bitrate" +msgstr "Taxa de bits" + +#: front/src/components/Sidebar.vue:18 src/components/library/Library.vue:4 +msgid "Browse" +msgstr "Buscar" + +#: front/src/components/Sidebar.vue:50 +msgid "Browse library" +msgstr "Buscar na biblioteca" + +#: front/src/components/library/Artists.vue:4 +msgid "Browsing artists" +msgstr "Buscando artistas" + +#: front/src/views/federation/LibraryTrackList.vue:3 +msgid "Browsing federated tracks" +msgstr "Buscando cancións federadas" + +#: front/src/views/federation/LibraryFollowersList.vue:3 +msgid "Browsing followers" +msgstr "Buscando seguidoras" + +#: front/src/views/federation/LibraryList.vue:3 +msgid "Browsing libraries" +msgstr "Buscando nas bibliotecas" + +#: front/src/views/playlists/List.vue:3 +msgid "Browsing playlists" +msgstr "Buscando nas listas de reprodución" + +#: front/src/components/library/Radios.vue:4 +msgid "Browsing radios" +msgstr "Buscando radios" + +#: front/src/components/library/radios/Builder.vue:5 +msgid "Builder" +msgstr "Construtor" + +#: front/src/components/audio/album/Card.vue:13 +msgid "By %{ artist }" +msgstr "De %{ artist }" + +#: front/src/components/federation/LibraryFollowTable.vue:57 +msgid "By confirming, %{ username } will be denied access to your library." +msgstr "Ao confirmar, %{ username } non terá acceso a súa biblioteca." + +#: front/src/components/federation/LibraryFollowTable.vue:73 +msgid "By confirming, %{ username } will be granted access to your library." +msgstr "Ao confirmar, %{ username } terá acceso a súa biblioteca." + +#: front/src/components/audio/track/Table.vue:43 +#: front/src/components/common/DangerousButton.vue:17 +#: front/src/components/library/radios/Filter.vue:53 +#: front/src/components/playlists/PlaylistModal.vue:63 +msgid "Cancel" +msgstr "Cancelar" + +#: front/src/components/library/radios/Builder.vue:59 +msgid "Candidates" +msgstr "Candidatas" + +#: front/src/components/auth/Settings.vue:76 +msgid "Cannot change your password" +msgstr "Non pode cambiar o contrasinal" + +#: front/src/App.vue:66 +msgid "Change language" +msgstr "Cambiar idioma" + +#: front/src/components/auth/Settings.vue:67 +msgid "Change my password" +msgstr "Cambiar o contrasinal" + +#: front/src/components/auth/Settings.vue:95 +msgid "Change password" +msgstr "Cambiar contrasinal" + +#: front/src/views/auth/PasswordResetConfirm.vue:4 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +msgid "Change your password" +msgstr "Cambiar o seu contrasinal" + +#: front/src/components/auth/Settings.vue:96 +msgid "Change your password?" +msgstr "Cambiar o contrasinal?" + +#: front/src/components/playlists/Editor.vue:21 +msgid "Changes synced with server" +msgstr "Cambios sincronizados co servidor" + +#: front/src/components/auth/Settings.vue:70 +msgid "Changing your password will also change your Subsonic API password if you have requested one." +msgstr "" +"Ao cambiar o contrasinal tamén cambia o seu contrasinal no API Subsonic si é " +"que solicitou un." + +#: front/src/components/auth/Settings.vue:98 +msgid "Changing your password will have the following consequences" +msgstr "Cambiar o contrasinal terá as seguintes consecuencias" + +#: front/src/App.vue:4 +msgid "Choose your instance" +msgstr "Escolla a súa instancia" + +#: front/src/components/Home.vue:64 +msgid "Clean library" +msgstr "Limpar biblioteca" + +#: front/src/components/manage/users/InvitationForm.vue:37 +msgid "Clear" +msgstr "Limpar" + +#: front/src/components/playlists/Editor.vue:40 +#: front/src/components/playlists/Editor.vue:45 +msgid "Clear playlist" +msgstr "Limpar lista reprodución" + +#: front/src/components/audio/Player.vue:270 +msgid "Clear your queue" +msgstr "Limpar cola de reprodución" + +#: front/src/components/library/import/BatchList.vue:23 +msgid "CLI" +msgstr "CLI" + +#: front/src/components/Home.vue:44 +msgid "Click once, listen for hours using built-in radios" +msgstr "Pulse unha vez, escoite durante horas utilizando as radios incrustadas" + +#: front/src/components/manage/library/RequestsTable.vue:30 +#: front/src/components/manage/library/RequestsTable.vue:64 +msgid "Closed" +msgstr "Pechado" + +#: front/src/components/manage/users/InvitationForm.vue:26 +#: front/src/components/manage/users/InvitationsTable.vue:42 +msgid "Code" +msgstr "Código" + +#: front/src/components/audio/album/Card.vue:43 +#: front/src/components/audio/artist/Card.vue:33 +#: front/src/components/discussion/Comment.vue:20 +msgid "Collapse" +msgstr "Pechar" + +#: front/src/components/manage/library/RequestsTable.vue:51 +#: front/src/components/requests/Form.vue:14 +msgid "Comment" +msgstr "Comentar" + +#: front/src/components/library/radios/Builder.vue:58 +msgid "Config" +msgstr "Configurar" + +#: front/src/components/common/DangerousButton.vue:21 +msgid "Confirm" +msgstr "Confirmar" + +#: front/src/views/auth/EmailConfirm.vue:4 src/views/auth/EmailConfirm.vue:20 +#: front/src/views/auth/EmailConfirm.vue:51 +msgid "Confirm your email" +msgstr "Confirme o seu correo-e" + +#: front/src/views/auth/EmailConfirm.vue:13 +msgid "Confirmation code" +msgstr "Código de confirmación" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "Copiar cancións da cola actual a lista de reprodución" + +#: front/src/components/Home.vue:88 +msgid "Covers, lyrics, our goal is to have them all ;)" +msgstr "Portadas, letras, o noso obxetivo é telas todas ;)" + +#: front/src/components/auth/Signup.vue:4 +msgid "Create a funkwhale account" +msgstr "Crear unha conta funkwhale" + +#: front/src/components/playlists/Form.vue:2 +msgid "Create a new playlist" +msgstr "Crear unha nova lista de reprodución" + +#: front/src/components/auth/Login.vue:17 +msgid "Create an account" +msgstr "Crear unha conta" + +#: front/src/components/manage/library/RequestsTable.vue:88 +#: front/src/components/requests/Card.vue:25 +msgid "Create import" +msgstr "Crear importación" + +#: front/src/components/auth/Signup.vue:51 +msgid "Create my account" +msgstr "Crear a miña conta" + +#: front/src/components/playlists/Form.vue:34 +msgid "Create playlist" +msgstr "Crear lista reprodución" + +#: front/src/components/library/Radios.vue:23 +msgid "Create your own radio" +msgstr "Cree a súa propia radio" + +#: front/src/components/federation/LibraryFollowTable.vue:22 +#: front/src/components/manage/library/RequestsTable.vue:52 +#: front/src/components/manage/users/InvitationsTable.vue:40 +msgid "Creation date" +msgstr "Data de creación" + +#: front/src/components/auth/Settings.vue:54 +msgid "Current avatar" +msgstr "Avatar actual" + +#: front/src/components/playlists/PlaylistModal.vue:8 +msgid "Current track" +msgstr "Canción actual" + +#: front/src/components/manage/library/FilesTable.vue:189 +#: front/src/components/manage/library/RequestsTable.vue:195 +#: front/src/components/manage/users/InvitationsTable.vue:166 +#: front/src/views/playlists/Detail.vue:33 +msgid "Delete" +msgstr "Borrar" + +#: front/src/views/playlists/Detail.vue:38 +msgid "Delete playlist" +msgstr "Borrar lista de reprodución" + +#: front/src/views/radios/Detail.vue:28 +msgid "Delete radio" +msgstr "Borrar radio" + +#: front/src/components/federation/LibraryFollowTable.vue:52 +#: front/src/components/federation/LibraryFollowTable.vue:63 +msgid "Deny" +msgstr "Denegar" + +#: front/src/components/federation/LibraryFollowTable.vue:54 +msgid "Deny access?" +msgstr "Denegar acceso?" + +#: front/src/components/favorites/List.vue:34 +#: front/src/components/federation/LibraryTrackTable.vue:29 +#: front/src/components/library/Artists.vue:26 src/components/library/Radios.vue:47 +#: front/src/components/manage/library/FilesTable.vue:20 +#: front/src/components/manage/library/RequestsTable.vue:20 +#: front/src/components/manage/users/UsersTable.vue:20 +#: front/src/views/federation/LibraryList.vue:29 src/views/playlists/List.vue:28 +msgid "Descending" +msgstr "Descendente" + +#: front/src/components/federation/LibraryCard.vue:50 +msgid "Detail" +msgstr "Detalle" + +#: front/src/views/admin/users/UsersDetail.vue:146 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "" +"Determina si a conta de usuaria está activa ou non. Usuarias inactivas non " +"poden conectar ou utilizar o servizo." + +#: front/src/components/auth/Settings.vue:104 +#: front/src/components/auth/SubsonicTokenForm.vue:52 +msgid "Disable access" +msgstr "Desactivar o acceso" + +#: front/src/components/auth/SubsonicTokenForm.vue:49 +msgid "Disable Subsonic access" +msgstr "Desactivar o acceso Subsonic" + +#: front/src/components/auth/SubsonicTokenForm.vue:50 +msgid "Disable Subsonic API access?" +msgstr "Desactivar o acceso a API Subsonic?" + +#: front/src/components/auth/SubsonicTokenForm.vue:14 +msgid "Discover how to use Funkwhale from other apps" +msgstr "Descubra cómo utilizar Funkwhale desde outras apps" + +#: front/src/components/library/radios/Builder.vue:26 +msgid "Display publicly" +msgstr "Mostrar públicamente" + +#: front/src/components/playlists/Editor.vue:42 +msgid "Do you want to clear the playlist \"%{ playlist }\"?" +msgstr "Quere baldeirar a lista de reprodución \"%{ playlist }\"?" + +#: front/src/components/common/DangerousButton.vue:7 +msgid "Do you want to confirm this action?" +msgstr "Quere confirmar esta acción?" + +#: front/src/views/playlists/Detail.vue:35 +msgid "Do you want to delete the playlist \"%{ playlist }\"?" +msgstr "Quere eliminar esta lista de reprodución \"%{ playlist }\"?" + +#: front/src/views/radios/Detail.vue:26 +msgid "Do you want to delete the radio \"%{ radio }\"?" +msgstr "Quere eliminar a radio \"%{ radio }\"?" + +#: front/src/components/common/ActionTable.vue:29 +msgid "Do you want to launch %{ action } on %{ count } element?" +msgid_plural "Do you want to launch %{ action } on %{ count } elements?" +msgstr[0] "Quere executar %{ action } sobre %{ count } elemento?" +msgstr[1] "Quere executar %{ action } sobre %{ count } elementos?" + +#: front/src/components/Sidebar.vue:113 +msgid "Do you want to restore your previous queue?" +msgstr "Quere restaurar a súa cola anterior?" + +#: front/src/App.vue:38 +msgid "Documentation" +msgstr "Documentación" + +#: front/src/components/audio/track/Table.vue:24 src/components/library/Track.vue:48 +msgid "Download" +msgstr "Descargar" + +#: front/src/components/audio/track/Table.vue:27 +msgid "Download tracks" +msgstr "Descargar cancións" + +#: front/src/components/playlists/Editor.vue:49 +msgid "Drag and drop rows to reorder tracks in the playlist" +msgstr "Arrastre e solte filas para reordenar as cancións na lista" + +#: front/src/components/library/Track.vue:58 +#: front/src/components/manage/library/FilesTable.vue:43 +msgid "Duration" +msgstr "Duración" + +#: front/src/components/Home.vue:96 +msgid "Easy to use" +msgstr "Fácil de utilizar" + +#: front/src/components/About.vue:21 +msgid "Edit instance info" +msgstr "Editar a info da instancia" + +#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +msgid "Edit..." +msgstr "Editar..." + +#: front/src/components/auth/Signup.vue:29 +#: front/src/components/manage/users/UsersTable.vue:38 +msgid "Email" +msgstr "Correo-e" + +#: front/src/views/admin/users/UsersDetail.vue:29 +msgid "Email address" +msgstr "Enderezo de correo" + +#: front/src/views/auth/EmailConfirm.vue:23 +msgid "Email confirmed" +msgstr "Correo-e confirmado" + +#: front/src/views/playlists/Detail.vue:29 +msgid "End edition" +msgstr "Finalizar edición" + +#: front/src/components/library/import/FileUpload.vue:4 +msgid "Ensure your music files are properly tagged before uploading them." +msgstr "" +"Asegúrese de que os ficheiros de música están etiquetados de xeito adecuado " +"antes de subilos." + +#: front/src/components/library/Radios.vue:140 +msgid "Enter a radio name..." +msgstr "Introduza un nome de unha radio..." + +#: front/src/components/library/Artists.vue:118 +msgid "Enter an artist name..." +msgstr "Introduza un nome de un artista..." + +#: front/src/views/federation/LibraryList.vue:122 +msgid "Enter an library domain name..." +msgstr "Introduza un nome de dominio de biblioteca..." + +#: front/src/views/playlists/List.vue:104 +msgid "Enter an playlist name..." +msgstr "Introduza un nome de lista de reprodución..." + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "Introduza o seu correo-e" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "Introduza o seu código de convite (dif. maiúsculas)" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "Introduza os termos de busca..." + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "Introduza o nome de usuaria" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "Introduza o nome de usuaria ou correo-e" + +#: front/src/components/auth/SubsonicTokenForm.vue:20 +msgid "Error" +msgstr "Fallo" + +#: front/src/views/admin/Settings.vue:87 +msgid "Error reporting" +msgstr "Fallo ao informar" + +#: front/src/components/common/ActionTable.vue:75 +msgid "Error while applying action" +msgstr "Fallo mentres se aplicaba a acción" + +#: front/src/views/auth/PasswordReset.vue:7 +msgid "Error while asking for a password reset" +msgstr "Fallo ao solicitar o restablecemento do contrasinal" + +#: front/src/views/auth/PasswordResetConfirm.vue:7 +msgid "Error while changing your password" +msgstr "Fallo ao intentar cambiar o contrasinal" + +#: front/src/views/auth/EmailConfirm.vue:7 +msgid "Error while confirming your email" +msgstr "Fallo ao confirmar o enderezo de correo" + +#: front/src/components/manage/users/InvitationForm.vue:4 +msgid "Error while creating invitation" +msgstr "Fallo ao crear o convite" + +#: front/src/components/admin/SettingsGroup.vue:5 +msgid "Error while saving settings" +msgstr "Fallo ao gardar os axustes" + +#: front/src/components/federation/LibraryForm.vue:10 +msgid "Error while scanning library" +msgstr "Fallo ao escanear a biblioteca" + +#: front/src/components/library/import/BatchDetail.vue:41 +#: front/src/components/library/import/BatchDetail.vue:70 +#: front/src/components/library/import/BatchList.vue:15 +msgid "Errored" +msgstr "Con fallo" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "Todas" + +#: front/src/components/playlists/Form.vue:85 +msgid "Everyone on this instance" +msgstr "Todas en esta instancia" + +#: front/src/components/library/radios/Builder.vue:57 +msgid "Exclude" +msgstr "Excluír" + +#: front/src/components/discussion/Comment.vue:14 +msgid "Expand" +msgstr "Despregar" + +#: front/src/components/manage/users/InvitationsTable.vue:41 +msgid "Expiration date" +msgstr "Data de caducidade" + +#: front/src/components/manage/users/InvitationsTable.vue:50 +msgid "Expired" +msgstr "Caducado" + +#: front/src/components/manage/users/InvitationsTable.vue:21 +msgid "Expired/used" +msgstr "Caducado/utilizado" + +#: front/src/components/library/import/Main.vue:65 +msgid "External source. Supported backends" +msgstr "Fonte externa. Xestións soportadas" + +#: front/src/components/Sidebar.vue:51 +msgid "Favorites" +msgstr "Favoritas" + +#: front/src/components/federation/LibraryForm.vue:3 +msgid "Federate with a new instance" +msgstr "Federar con unha nova instancia" + +#: front/src/views/federation/LibraryTrackList.vue:21 +msgid "Federated tracks" +msgstr "Cancións federadas" + +#: front/src/components/Sidebar.vue:87 src/components/library/import/BatchList.vue:25 +#: front/src/components/manage/users/UsersTable.vue:180 +#: front/src/views/admin/Settings.vue:84 src/views/admin/users/UsersDetail.vue:161 +#: front/src/views/federation/Base.vue:35 src/views/federation/LibraryDetail.vue:40 +msgid "Federation" +msgstr "Federación" + +#: front/src/views/federation/LibraryDetail.vue:3 +msgid "File mirroring" +msgstr "Espellado de ficheiros" + +#: front/src/components/library/import/FileUpload.vue:43 +msgid "File name" +msgstr "Nome do ficheiro" + +#: front/src/components/library/import/Main.vue:76 +msgid "File upload" +msgstr "Subir ficheiro" + +#: front/src/views/admin/library/Base.vue:5 src/views/admin/library/FilesList.vue:21 +msgid "Files" +msgstr "Ficheiros" + +#: front/src/components/library/import/ArtistImport.vue:7 +msgid "Filter album types" +msgstr "Filtrar tipos de álbume" + +#: front/src/components/library/radios/Builder.vue:56 +msgid "Filter name" +msgstr "Nome do filtro" + +#: front/src/components/library/import/Main.vue:52 +msgid "Finish import" +msgstr "Rematar importación" + +#: front/src/components/library/import/BatchDetail.vue:54 +msgid "Finished" +msgstr "Rematado" + +#: front/src/components/library/import/Main.vue:59 +msgid "First, choose where you want to import the music from" +msgstr "Primeiro, escolla de onde quere importar a música" + +#: front/src/components/federation/LibraryCard.vue:44 +msgid "Follow" +msgstr "Seguir" + +#: front/src/components/federation/LibraryCard.vue:36 +msgid "Follow request pending approval" +msgstr "Solicitude de seguimento pendente de aprobación" + +#: front/src/views/federation/LibraryDetail.vue:21 +msgid "Follow status" +msgstr "Estado do seguimento" + +#: front/src/views/federation/Base.vue:13 +#: front/src/views/federation/LibraryFollowersList.vue:24 +msgid "Followers" +msgstr "Seguidoras" + +#: front/src/components/federation/LibraryCard.vue:18 +msgid "Followers only" +msgstr "Só seguidoras" + +#: front/src/components/federation/LibraryCard.vue:15 +#: front/src/views/federation/LibraryDetail.vue:29 +msgid "Following" +msgstr "Seguindo" + +#: front/src/components/activity/Like.vue:12 src/components/activity/Listen.vue:12 +msgid "from %{ album } by %{ artist }" +msgstr "de %{ album } por %{ artist }" + +#: front/src/components/library/Track.vue:13 +msgid "From album %{ album } by %{ artist }" +msgstr "Do álbume %{ album } de %{ artist }" + +#: front/src/App.vue:56 +msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" +msgstr "" +"Funkwhale é un proxecto de software libre e aberto xestionado por " +"voluntarias. Pode axudarnos a mellorar a plataforma informando de fallos, " +"suxerindo características e compartindo o proxecto coas amizades!" + +#: front/src/components/auth/SubsonicTokenForm.vue:7 +msgid "Funkwhale is compatible with other music players that support the Subsonic API." +msgstr "" +"Funkwhale é compatible con outros reprodutores de música compatibles coa API " +"Subsonic." + +#: front/src/components/Home.vue:98 +msgid "Funkwhale is dead simple to use." +msgstr "Funckwhale élle ben doado de utilizar." + +#: front/src/components/Home.vue:39 +msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." +msgstr "" +"Funkwhale está deseñado para escoitar facilmente a música que lle gusta ou " +"descubrir novos artistas." + +#: front/src/components/Home.vue:119 +msgid "Funkwhale is free and gives you control on your music." +msgstr "Funkwhale é gratuíto e dalle o control sobre a súa música." + +#: front/src/components/Home.vue:66 +msgid "Funkwhale takes care of handling your music" +msgstr "Funkwhale ocúpase de xestionar a súa música" + +#: front/src/components/manage/users/InvitationForm.vue:16 +msgid "Get a new invitation" +msgstr "Obter un novo convite" + +#: front/src/components/Home.vue:13 +msgid "Get me to the library" +msgstr "Lévame a biblioteca" + +#: front/src/components/Home.vue:77 +msgid "" +"Get quality metadata about your music thanks to\n" +" \n" +" MusicBrainz\n" +" " +msgstr "" +"Obteña metadatos de calidade sobre a súa música grazas a\n" +" \n" +" MusicBrainz\n" +" " + +#: front/src/components/common/ActionTable.vue:21 +#: front/src/components/common/ActionTable.vue:27 +msgid "Go" +msgstr "Ir" + +#: front/src/components/PageNotFound.vue:14 +msgid "Go to home page" +msgstr "Ir ao inicio" + +#: front/src/components/library/import/Main.vue:13 +msgid "Grab corresponding metadata" +msgstr "Colla os metadatos correspondentes" + +#: front/src/App.vue:74 +msgid "Help us translate Funkwhale" +msgstr "Axúdenos a traducir Funkwhale" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "Inicio" + +#: front/src/components/instance/Stats.vue:36 +msgid "Hours of music" +msgstr "Horas de música" + +#: front/src/components/auth/SubsonicTokenForm.vue:11 +msgid "However, accessing Funkwhale from those clients require a separate password you can set below." +msgstr "" +"Porén, o acceso a Funkwhale desde estos outros clientes precisa un " +"contrasinal separado que pode establecer aquí abaixo." + +#: front/src/components/library/import/BatchList.vue:34 +msgid "ID" +msgstr "ID" + +#: front/src/views/auth/PasswordResetConfirm.vue:24 +msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." +msgstr "" +"Si o enderezo de correo proporcionado no paso anterior é válido e ligado a " +"unha conta de usuaria, debería recibir un correo coas instrucións de " +"restablecemento nun par de minutos." + +#: front/src/components/federation/LibraryTrackTable.vue:196 +#: front/src/components/library/Library.vue:17 +msgid "Import" +msgstr "Importar" + +#: front/src/components/federation/LibraryTrackTable.vue:57 +msgid "Import #%{ id } launched" +msgstr "Importación #%{ id } iniciada" + +#: front/src/components/library/import/Main.vue:38 +msgid "Import %{ count } track" +msgid_plural "Import %{ count } tracks" +msgstr[0] "Importar %{ count } canción" +msgstr[1] "Importar %{ count } cancións" + +#: front/src/components/library/import/BatchDetail.vue:10 +msgid "Import batch" +msgstr "Importar lote" + +#: front/src/components/library/import/BatchDetail.vue:185 +msgid "Import Batch #%{ id }" +msgstr "Importar Lote #%{ id }" + +#: front/src/components/library/Library.vue:20 +msgid "Import batches" +msgstr "Lotes de importación" + +#: front/src/components/library/import/BatchList.vue:117 +msgid "Import Batches" +msgstr "Lotes de Importación" + +#: front/src/components/manage/library/FilesTable.vue:40 +#: front/src/components/manage/library/RequestsTable.vue:53 +msgid "Import date" +msgstr "Data de importación" + +#: front/src/components/library/import/FileUpload.vue:38 +msgid "Import detail page" +msgstr "Páxina de detalle da importación" + +#: front/src/components/Sidebar.vue:81 +msgid "Import music" +msgstr "Importar música" + +#: front/src/components/library/import/Main.vue:267 +msgid "Import Music" +msgstr "Importar Música" + +#: front/src/components/Home.vue:71 +msgid "Import music from various platforms, such as YouTube or SoundCloud" +msgstr "" +"Importar música desde varias plataformas, tales como YouTube ou SoundCloud" + +#: front/src/components/federation/LibraryTrackTable.vue:14 +#: front/src/components/federation/LibraryTrackTable.vue:66 +msgid "Import pending" +msgstr "Pendente de importación" + +#: front/src/views/admin/library/Base.vue:9 +#: front/src/views/admin/library/RequestsList.vue:3 +#: front/src/views/admin/library/RequestsList.vue:21 +msgid "Import requests" +msgstr "Peticións de importación" + +#: front/src/components/library/import/BatchList.vue:20 +#: front/src/components/library/import/Main.vue:6 +msgid "Import source" +msgstr "Fonte de importación" + +#: front/src/components/federation/LibraryTrackTable.vue:9 +msgid "Import status" +msgstr "Estado da importación" + +#: front/src/components/library/import/ReleaseImport.vue:14 +msgid "Import this release" +msgstr "Importar esta publicación" + +#: front/src/components/library/import/TrackImport.vue:11 +msgid "Import this track" +msgstr "Importar esta canción" + +#: front/src/components/federation/LibraryTrackTable.vue:12 +#: front/src/components/manage/library/RequestsTable.vue:29 +#: front/src/components/manage/library/RequestsTable.vue:61 +msgid "Imported" +msgstr "Importado" + +#: front/src/components/library/import/TrackImport.vue:44 +msgid "Imported URL" +msgstr "URL importado" + +#: front/src/views/admin/Settings.vue:82 +msgid "Imports" +msgstr "Importancións" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:3 +msgid "In favorites" +msgstr "Nas favoritas" + +#: front/src/components/federation/LibraryTrackTable.vue:65 +msgid "In library" +msgstr "Na biblioteca" + +#: front/src/components/manage/users/UsersTable.vue:54 +msgid "Inactive" +msgstr "Non activo" + +#: front/src/components/library/import/Main.vue:96 +msgid "Input a MusicBrainz ID manually:" +msgstr "Introducir un ID MusicBrainz manualmente:" + +#: front/src/views/auth/PasswordReset.vue:53 +msgid "Input the email address binded to your account" +msgstr "Introducir o enderezo de correo ligado a súa conta" + +#: front/src/components/playlists/Editor.vue:31 +msgid "Insert from queue (%{ count } track)" +msgid_plural "Insert from queue (%{ count } tracks)" +msgstr[0] "Introducir desde a cola (%{ count } canción)" +msgstr[1] "Introducir desde a cola (%{ count } cancións)" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "Información da instancia" + +#: front/src/components/library/Radios.vue:9 +msgid "Instance radios" +msgstr "Radios da instancia" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "Axustes da instancia" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "Liña temporal da instancia" + +#: front/src/components/auth/Signup.vue:42 +#: front/src/components/manage/users/InvitationForm.vue:11 +msgid "Invitation code" +msgstr "Código do convite" + +#: front/src/components/auth/Signup.vue:43 +msgid "Invitation code (optional)" +msgstr "Código do convite (optativo)" + +#: front/src/views/admin/users/Base.vue:8 src/views/admin/users/InvitationsList.vue:3 +#: front/src/views/admin/users/InvitationsList.vue:24 +msgid "Invitations" +msgstr "Convites" + +#: front/src/App.vue:43 +msgid "Issue tracker" +msgstr "Seguimento de problemas" + +#: front/src/components/library/import/BatchDetail.vue:80 +msgid "Job ID" +msgstr "ID do traballo" + +#: front/src/components/library/import/BatchList.vue:36 +msgid "Jobs" +msgstr "Traballos" + +#: front/src/components/Home.vue:50 +msgid "Keep a track of your favorite songs" +msgstr "Faga seguimento das súas cancións favoritas" + +#: front/src/components/audio/track/Table.vue:33 +msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." +msgstr "Manteña o TESTEMUÑO_PRIVADO segredamente xa que da acceso a súa conta." + +#: front/src/components/manage/users/UsersTable.vue:41 +#: front/src/views/admin/users/UsersDetail.vue:45 +msgid "Last activity" +msgstr "Última actividade" + +#: front/src/views/federation/LibraryDetail.vue:101 +msgid "Last fetched" +msgstr "Última obtida" + +#: front/src/components/playlists/PlaylistModal.vue:32 +msgid "Last modification" +msgstr "Última modificación" + +#: front/src/components/common/ActionTable.vue:39 +msgid "Launch" +msgstr "Lanzar" + +#: front/src/components/library/import/BatchDetail.vue:18 +#: front/src/components/library/import/BatchList.vue:35 +msgid "Launch date" +msgstr "Data de lanzamento" + +#: front/src/components/federation/LibraryForm.vue:31 +msgid "Launch scan" +msgstr "Lanzar escaneado" + +#: front/src/components/Home.vue:10 +msgid "Learn more about this instance" +msgstr "Coñeza máis sobre esta instancia" + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "Deixar baldeiro para un código aleatorio" + +#: front/src/components/requests/Form.vue:10 +msgid "Leave this field empty if you're requesting the whole discography." +msgstr "Deixe este campo baldeiro si está solicitando a discografía completa." + +#: front/src/views/federation/Base.vue:5 src/views/federation/LibraryList.vue:123 +msgid "Libraries" +msgstr "Bibliotecas" + +#: front/src/components/Sidebar.vue:70 +#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/components/instance/Stats.vue:30 +#: front/src/components/manage/users/UsersTable.vue:176 +#: front/src/views/admin/users/UsersDetail.vue:157 +#: front/src/views/federation/LibraryDetail.vue:194 +msgid "Library" +msgstr "Biblioteca" + +#: front/src/views/admin/library/FilesList.vue:3 +msgid "Library files" +msgstr "Ficheiros de biblioteca" + +#: front/src/components/federation/LibraryForm.vue:20 +msgid "Library name" +msgstr "Nome da biblioteca" + +#: front/src/views/federation/LibraryDetail.vue:84 +msgid "Library size" +msgstr "Tamaño da biblioteca" + +#: front/src/components/federation/LibraryForm.vue:96 +msgid "library@demo.funkwhale.audio" +msgstr "library@demo.funkwhale.audio" + +#: front/src/App.vue:29 +msgid "Links" +msgstr "Ligazóns" + +#: front/src/views/instance/Timeline.vue:4 +msgid "Loading timeline..." +msgstr "Cargando liña temporal..." + +#: front/src/components/favorites/List.vue:5 +msgid "Loading your favorites..." +msgstr "Cargando favoritas..." + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "Conectar" + +#: front/src/components/auth/Login.vue:4 +msgid "Log in to your Funkwhale account" +msgstr "Conecte coa súa conta Funkwhale" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "Desconectar" + +#: front/src/components/Sidebar.vue:38 +msgid "Logged in as %{ username }" +msgstr "Conectada como %{ username }" + +#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +msgid "Login" +msgstr "Conectar" + +#: front/src/components/Sidebar.vue:43 +msgid "Logout" +msgstr "Desconectar" + +#: front/src/components/audio/Player.vue:266 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "Bucle desactivado. Pulse para cambiar ao bucle de unha soa canción." + +#: front/src/components/audio/Player.vue:267 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "Bucle de unha canción. Pulse para cambiar a bucle de toda a cola." + +#: front/src/components/audio/Player.vue:268 +msgid "Looping on whole queue. Click to disable looping." +msgstr "Bucle de toda a cola. Pulse para desactivar o bucle." + +#: front/src/components/library/Track.vue:94 +msgid "Lyrics" +msgstr "Letras" + +#: front/src/views/admin/library/Base.vue:25 +msgid "Manage library" +msgstr "Xestionar biblioteca" + +#: front/src/components/playlists/PlaylistModal.vue:3 +msgid "Manage playlists" +msgstr "Xestionar listas de reprodución" + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "Xestionar usuarias" + +#: front/src/views/playlists/List.vue:8 +msgid "Manage your playlists" +msgstr "Xestionar as súas listas de reprodución" + +#: front/src/components/manage/library/RequestsTable.vue:197 +msgid "Mark as closed" +msgstr "Marcar como pechado" + +#: front/src/components/manage/library/RequestsTable.vue:196 +msgid "Mark as imported" +msgstr "Marcar como importado" + +#: front/src/components/library/import/Main.vue:12 +msgid "Metadata" +msgstr "Metadatos" + +#: front/src/components/library/import/Main.vue:115 +msgid "" +"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" +" \n" +" MusicBrainz\n" +" \n" +" project, which you can think about as the Wikipedia of music." +msgstr "" +"Os metadatos son os datos relacionados coa música que quere importar. Esto " +"inclúe toda a información sobre os artistas, álbumes e cancións. Para poder " +"ter unha boa biblioteca recoméndase obter os datos desde o proxecto\n" +" \n" +" MusicBrainz\n" +" \n" +" que pode considerarse como a Wikipedia da música." + +#: front/src/components/Sidebar.vue:48 src/components/library/import/Main.vue:18 +msgid "Music" +msgstr "Música" + +#: front/src/components/library/import/Main.vue:147 +msgid "Music request" +msgstr "Petición de música" + +#: front/src/components/audio/Player.vue:265 +msgid "Mute" +msgstr "Acalar" + +#: front/src/components/Sidebar.vue:34 +msgid "My account" +msgstr "A miña conta" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "A miña fantástica lista" + +#: front/src/components/library/radios/Builder.vue:227 +msgid "My awesome radio" +msgstr "A miña increíble radio" + +#: front/src/components/library/Track.vue:64 src/components/library/Track.vue:75 +#: front/src/components/library/Track.vue:86 +#: front/src/components/manage/library/FilesTable.vue:63 +#: front/src/components/manage/library/FilesTable.vue:69 +#: front/src/components/manage/library/FilesTable.vue:75 +#: front/src/components/manage/library/FilesTable.vue:81 +#: front/src/components/manage/library/RequestsTable.vue:71 +#: front/src/components/manage/library/RequestsTable.vue:75 +#: front/src/components/manage/library/RequestsTable.vue:82 +#: front/src/components/manage/users/UsersTable.vue:61 +#: front/src/views/admin/users/UsersDetail.vue:49 +msgid "N/A" +msgstr "N/A" + +#: front/src/components/playlists/PlaylistModal.vue:31 +#: front/src/views/admin/users/UsersDetail.vue:21 +msgid "Name" +msgstr "Nome" + +#: front/src/components/auth/Settings.vue:88 +#: front/src/views/auth/PasswordResetConfirm.vue:14 +msgid "New password" +msgstr "Novo contrasinal" + +#: front/src/components/Sidebar.vue:158 +msgid "New tracks will be appended here automatically." +msgstr "As novas cancións engadiranse aquí automáticamente." + +#: front/src/components/library/import/Main.vue:29 +msgid "Next step" +msgstr "Seguinte paso" + +#: front/src/components/audio/Player.vue:263 +msgid "Next track" +msgstr "Seguinte canción" + +#: front/src/components/Sidebar.vue:125 +msgid "No" +msgstr "Non" + +#: front/src/components/Home.vue:103 +msgid "No add-ons, no plugins : you only need a web library" +msgstr "Sin engadidos nin complementos: só precisa unha biblioteca na web" + +#: front/src/components/library/Track.vue:102 +msgid "No lyrics available for this track." +msgstr "Non hai letras dispoñibles para esta canción." + +#: front/src/components/playlists/Form.vue:81 +msgid "Nobody except me" +msgstr "Ninguén excepto eu" + +#: front/src/views/federation/LibraryDetail.vue:32 +msgid "Not following" +msgstr "Non seguindo" + +#: front/src/components/federation/LibraryTrackTable.vue:13 +#: front/src/components/federation/LibraryTrackTable.vue:67 +msgid "Not imported" +msgstr "Non importado" + +#: front/src/components/manage/users/InvitationsTable.vue:51 +msgid "Not used" +msgstr "Non utilizado" + +#: front/src/App.vue:37 +msgid "Official website" +msgstr "Sitio web oficial" + +#: front/src/components/auth/Settings.vue:83 +msgid "Old password" +msgstr "Contrasinal antigo" + +#: front/src/components/library/import/FileUpload.vue:36 +msgid "Once all your files are uploaded, simply click the following button to check the import status." +msgstr "" +"Unha vez subidos todos os ficheiros, só precisa pulsar o seguinte botón para " +"comprobar o estado da importación." + +#: front/src/components/federation/LibraryCard.vue:21 +#: front/src/components/manage/users/InvitationsTable.vue:20 +msgid "Open" +msgstr "Abrir" + +#: front/src/App.vue:63 +msgid "Options" +msgstr "Opcións" + +#: front/src/components/library/import/Main.vue:93 +msgid "Or" +msgstr "Ou" + +#: front/src/components/favorites/List.vue:23 +#: front/src/components/federation/LibraryTrackTable.vue:18 +#: front/src/components/library/Artists.vue:15 src/components/library/Radios.vue:33 +#: front/src/components/manage/library/FilesTable.vue:9 +#: front/src/components/manage/library/RequestsTable.vue:9 +#: front/src/components/manage/users/InvitationsTable.vue:9 +#: front/src/components/manage/users/UsersTable.vue:9 +#: front/src/views/federation/LibraryList.vue:18 src/views/playlists/List.vue:17 +msgid "Ordering" +msgstr "Ordenando" + +#: front/src/components/favorites/List.vue:31 +#: front/src/components/federation/LibraryTrackTable.vue:26 +#: front/src/components/library/Artists.vue:23 src/components/library/Radios.vue:41 +#: front/src/components/manage/library/FilesTable.vue:17 +#: front/src/components/manage/library/RequestsTable.vue:17 +#: front/src/components/manage/users/UsersTable.vue:17 +#: front/src/views/federation/LibraryList.vue:26 src/views/playlists/List.vue:25 +msgid "Ordering direction" +msgstr "Dirección da orde" + +#: front/src/components/manage/users/InvitationsTable.vue:38 +msgid "Owner" +msgstr "Dona" + +#: front/src/components/PageNotFound.vue:33 +msgid "Page Not Found" +msgstr "Páxina non atopada" + +#: front/src/components/PageNotFound.vue:7 +msgid "Page not found!" +msgstr "Non atopamos a páxina!" + +#: front/src/components/auth/Login.vue:32 src/components/auth/Signup.vue:38 +msgid "Password" +msgstr "Contrasinal" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "Contrasinal actualizado" + +#: front/src/views/auth/PasswordResetConfirm.vue:28 +msgid "Password updated successfully" +msgstr "Contrasinal actualizado correctamente" + +#: front/src/components/audio/Player.vue:262 +msgid "Pause track" +msgstr "Pausar canción" + +#: front/src/components/federation/LibraryFollowTable.vue:46 +#: front/src/components/library/import/BatchDetail.vue:33 +#: front/src/components/library/import/BatchDetail.vue:69 +#: front/src/components/library/import/BatchList.vue:14 +#: front/src/components/library/import/FileUpload.vue:59 +#: front/src/components/manage/library/RequestsTable.vue:27 +#: front/src/components/manage/library/RequestsTable.vue:63 +msgid "Pending" +msgstr "Pendente" + +#: front/src/components/federation/LibraryFollowTable.vue:11 +#: front/src/views/federation/LibraryDetail.vue:26 +msgid "Pending approval" +msgstr "Pendente de aceptación" + +#: front/src/components/Sidebar.vue:217 +msgid "Pending follow requests" +msgstr "Peticións de seguimento pendentes" + +#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 +msgid "Pending import requests" +msgstr "Peticións de importación pendentes" + +#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +msgid "Pending requests" +msgstr "Peticións pendentes" + +#: front/src/components/manage/users/UsersTable.vue:42 +#: front/src/views/admin/users/UsersDetail.vue:68 +msgid "Permissions" +msgstr "Permisos" + +#: front/src/components/audio/PlayButton.vue:9 src/components/library/Track.vue:30 +msgid "Play" +msgstr "Reproducir" + +#: front/src/components/audio/album/Card.vue:50 +#: front/src/components/audio/artist/Card.vue:44 src/components/library/Album.vue:28 +#: front/src/views/playlists/Detail.vue:23 +msgid "Play all" +msgstr "Reproducir todo" + +#: front/src/components/library/Artist.vue:26 +msgid "Play all albums" +msgstr "Reproducir todos os álbumes" + +#: front/src/components/audio/PlayButton.vue:58 +msgid "Play immediatly" +msgstr "Reproducir inmediatamente" + +#: front/src/components/audio/PlayButton.vue:15 +msgid "Play next" +msgstr "Reproducir seguinte" + +#: front/src/components/audio/PlayButton.vue:16 +msgid "Play now" +msgstr "Reproducir agora" + +#: front/src/components/audio/Player.vue:261 +msgid "Play track" +msgstr "Reproducir canción" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "Lista de reprodución" + +#: front/src/views/playlists/Detail.vue:12 +msgid "Playlist containing %{ count } track, by %{ username }" +msgid_plural "Playlist containing %{ count } tracks, by %{ username }" +msgstr[0] "Lista de reprodución que contén %{ count } canción, de %{ username }" +msgstr[1] "Lista de reprodución que contén %{ count } cancións, de %{ username }" + +#: front/src/components/playlists/Form.vue:9 +msgid "Playlist created" +msgstr "Lista creada" + +#: front/src/components/playlists/Editor.vue:4 +msgid "Playlist editor" +msgstr "Editora da lista" + +#: front/src/components/playlists/Form.vue:21 +msgid "Playlist name" +msgstr "Nome da lista" + +#: front/src/components/playlists/Form.vue:6 +msgid "Playlist updated" +msgstr "Lista de reprodución actualizada" + +#: front/src/components/playlists/Form.vue:25 +msgid "Playlist visibility" +msgstr "Visibilidade da lista de reprodución" + +#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 +#: front/src/views/playlists/List.vue:103 +msgid "Playlists" +msgstr "Listas de reprodución" + +#: front/src/components/Home.vue:56 +msgid "Playlists? We got them" +msgstr "Listas de reprodución? Témolas" + +#: front/src/components/auth/Settings.vue:79 +msgid "Please double-check your password is correct" +msgstr "Por favor, comprobe que o seu contrasinal é correcto" + +#: front/src/components/auth/Login.vue:9 +msgid "Please double-check your username/password couple is correct" +msgstr "Por favor, comprobe que o par usuaria/contrasinal é correcto" + +#: front/src/components/auth/Settings.vue:46 +msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." +msgstr "PNG, GIF ou JPG. 2MB como máximo. Será reducida a 400x400px." + +#: front/src/components/library/import/Main.vue:26 +msgid "Previous step" +msgstr "Paso anterior" + +#: front/src/components/audio/Player.vue:260 +msgid "Previous track" +msgstr "Canción anterior" + +#: front/src/views/auth/EmailConfirm.vue:26 +#: front/src/views/auth/PasswordResetConfirm.vue:31 +msgid "Proceed to login" +msgstr "Ir a conectar" + +#: front/src/components/federation/LibraryTrackTable.vue:50 +msgid "Published date" +msgstr "Data de publicación" + +#: front/src/components/library/import/ArtistImport.vue:17 +msgid "Query template" +msgstr "Modelo da consulta" + +#: front/src/components/Sidebar.vue:20 +msgid "Queue" +msgstr "Cola" + +#: front/src/components/audio/Player.vue:203 +msgid "Queue shuffled!" +msgstr "Cola barallada!" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "Radio" + +#: front/src/components/library/radios/Builder.vue:226 +msgid "Radio Builder" +msgstr "Constructor de Radio" + +#: front/src/components/library/radios/Builder.vue:15 +msgid "Radio created" +msgstr "Radio creada" + +#: front/src/components/library/radios/Builder.vue:21 +msgid "Radio name" +msgstr "Nome da Radio" + +#: front/src/components/library/radios/Builder.vue:12 +msgid "Radio updated" +msgstr "Radio actualizada" + +#: front/src/components/library/Library.vue:10 src/components/library/Radios.vue:141 +msgid "Radios" +msgstr "Radios" + +#: front/src/views/instance/Timeline.vue:7 +msgid "Recent activity on this instance" +msgstr "Actividade recente en esta instancia" + +#: front/src/components/library/Home.vue:24 +msgid "Recently added" +msgstr "Recentemente engadida" + +#: front/src/components/library/Home.vue:11 +msgid "Recently favorited" +msgstr "Favorecida recentemente" + +#: front/src/components/library/Home.vue:6 +msgid "Recently listened" +msgstr "Escoitada recentemente" + +#: front/src/components/library/import/BatchDetail.vue:81 +msgid "Recording MusicBrainz ID" +msgstr "Gravando ID MusicBrainz" + +#: front/src/views/admin/users/UsersDetail.vue:84 +#: front/src/views/federation/LibraryDetail.vue:122 +msgid "Refresh" +msgstr "Actualizar" + +#: front/src/components/federation/LibraryFollowTable.vue:42 +msgid "Refused" +msgstr "Rexeitado" + +#: front/src/components/auth/Profile.vue:12 +msgid "Registered since %{ date }" +msgstr "Rexistrada desde %{ date }" + +#: front/src/components/auth/Signup.vue:9 +msgid "Registration are closed on this instance, you will need an invitation code to signup." +msgstr "" +"O rexistro está pechado en esta instancia, necesita un código de convite " +"para rexistrarse." + +#: front/src/components/manage/users/UsersTable.vue:71 +msgid "regular user" +msgstr "usuaria normal" + +#: front/src/components/library/radios/Filter.vue:59 +msgid "Remove" +msgstr "Eliminar" + +#: front/src/components/auth/Settings.vue:58 +msgid "Remove avatar" +msgstr "Eliminar avatar" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "Eliminar das favoritas" + +#: front/src/components/auth/SubsonicTokenForm.vue:34 +#: front/src/components/auth/SubsonicTokenForm.vue:37 +msgid "Request a new password" +msgstr "Solicitar un novo contrasinal" + +#: front/src/components/auth/SubsonicTokenForm.vue:35 +msgid "Request a new Subsonic API password?" +msgstr "Solicitar un nonvo contrasinal para o API Subsonic?" + +#: front/src/components/auth/SubsonicTokenForm.vue:43 +msgid "Request a password" +msgstr "Solicitar un contrasinal" + +#: front/src/App.vue:35 +msgid "Request music" +msgstr "Solicitar música" + +#: front/src/views/library/MusicRequest.vue:4 src/views/library/MusicRequest.vue:21 +msgid "Request some music" +msgstr "Pedir algo de música" + +#: front/src/components/requests/Form.vue:20 +msgid "Request submitted!" +msgstr "Solicitude enviada!" + +#: front/src/components/library/import/BatchDetail.vue:49 +msgid "Rerun errored jobs" +msgstr "Voltar a executar os traballos fallidos" + +#: front/src/components/library/import/BatchDetail.vue:187 +msgid "Rerun job" +msgstr "Executar de novo a tarefa" + +#: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 +#: front/src/views/auth/PasswordReset.vue:52 +msgid "Reset your password" +msgstr "Restablecer o seu contrasinal" + +#: front/src/components/library/import/TrackImport.vue:31 +msgid "Result %{ current }/%{ total }" +msgstr "Resultado %{ current }/%{ total }" + +#: front/src/components/favorites/List.vue:38 src/components/library/Artists.vue:30 +#: front/src/components/library/Radios.vue:52 src/views/federation/LibraryList.vue:33 +#: front/src/views/playlists/List.vue:32 +msgid "Results per page" +msgstr "Resultados por páxina" + +#: front/src/components/admin/SettingsGroup.vue:63 +#: front/src/components/library/radios/Builder.vue:29 +msgid "Save" +msgstr "Gardar" + +#: front/src/views/federation/LibraryDetail.vue:112 +msgid "Scan triggered!" +msgstr "Escaneo activado!" + +#: front/src/components/federation/LibraryTrackTable.vue:5 +#: front/src/components/library/Artists.vue:10 src/components/library/Radios.vue:29 +#: front/src/components/library/import/BatchDetail.vue:62 +#: front/src/components/library/import/BatchList.vue:7 +#: front/src/components/manage/library/FilesTable.vue:5 +#: front/src/components/manage/library/RequestsTable.vue:5 +#: front/src/components/manage/users/InvitationsTable.vue:5 +#: front/src/components/manage/users/UsersTable.vue:5 +#: front/src/views/federation/LibraryList.vue:14 src/views/playlists/List.vue:13 +msgid "Search" +msgstr "Buscar" + +#: front/src/components/library/import/Main.vue:85 +msgid "Search an entity you want to import:" +msgstr "Buscar unha entidade que queira importar:" + +#: front/src/components/manage/library/RequestsTable.vue:180 +msgid "Search by artist, username, comment..." +msgstr "Buscar por artista, nome de usuaria, comentario..." + +#: front/src/components/library/import/BatchDetail.vue:188 +msgid "Search by source..." +msgstr "Buscar por fonte..." + +#: front/src/components/library/import/BatchList.vue:116 +msgid "Search by submitter, source..." +msgstr "Buscar por remitente, fonte..." + +#: front/src/components/federation/LibraryTrackTable.vue:182 +#: front/src/components/manage/library/FilesTable.vue:175 +msgid "Search by title, artist, domain..." +msgstr "Buscar por título, artista, dominio..." + +#: front/src/components/federation/LibraryFollowTable.vue:140 +msgid "Search by username, domain..." +msgstr "Buscar por nome de usuaria, dominio..." + +#: front/src/components/manage/users/InvitationsTable.vue:152 +msgid "Search by username, email, code..." +msgstr "Buscar por nome de usuaria, correo-e, código..." + +#: front/src/components/manage/users/UsersTable.vue:162 +msgid "Search by username, email, name..." +msgstr "Buscar por nome de usuaria, correo-e, nome..." + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "Buscar por artistas, álbumes, cancións..." + +#: front/src/components/audio/Search.vue:2 +msgid "Search for some music" +msgstr "Buscar por algo de música" + +#: front/src/components/library/Track.vue:105 +msgid "Search on lyrics.wikia.com" +msgstr "Buscar en lyrics.wikia.com" + +#: front/src/components/library/Album.vue:33 src/components/library/Artist.vue:31 +#: front/src/components/library/Track.vue:40 +msgid "Search on Wikipedia" +msgstr "Buscar en Wikipedia" + +#: front/src/components/library/import/TrackImport.vue:42 +msgid "Search query" +msgstr "Consulta da busca" + +#: front/src/views/admin/Settings.vue:15 +msgid "Sections" +msgstr "Seccións" + +#: front/src/components/library/radios/Builder.vue:41 +msgid "Select a filter" +msgstr "Escolla un filtro" + +#: front/src/components/common/ActionTable.vue:61 +msgid "Select all %{ total } elements" +msgid_plural "Select all %{ total } elements" +msgstr[0] "Escolla %{ total } elemento" +msgstr[1] "Escolla todos os %{ total } elementos" + +#: front/src/components/library/import/FileUpload.vue:22 +msgid "Select files to upload..." +msgstr "Escolla os ficheiros a subir..." + +#: front/src/components/common/ActionTable.vue:69 +msgid "Select only current page" +msgstr "Seleccionar só páxina actual" + +#: front/src/components/library/import/Main.vue:19 +msgid "Select relevant sources or files for import" +msgstr "Escolla fontes relevantes ou ficheiros a importar" + +#: front/src/components/federation/LibraryCard.vue:43 +msgid "Send a follow request" +msgstr "Enviar unha petición de seguimento" + +#: front/src/components/Sidebar.vue:97 src/components/manage/users/UsersTable.vue:184 +#: front/src/views/admin/users/UsersDetail.vue:165 +msgid "Settings" +msgstr "Axustes" + +#: front/src/components/auth/Settings.vue:10 +msgid "Settings updated" +msgstr "Axustes actualizados" + +#: front/src/components/admin/SettingsGroup.vue:11 +msgid "Settings updated successfully." +msgstr "Axustes actualizados correctamente." + +#: front/src/components/auth/Profile.vue:24 +msgid "Settings..." +msgstr "Axustes..." + +#: front/src/components/manage/users/InvitationForm.vue:27 +msgid "Share link" +msgstr "Compartir ligazón" + +#: front/src/components/audio/artist/Card.vue:30 +msgid "Show 1 more album" +msgid_plural "Show %{ count } more albums" +msgstr[0] "Mostrar 1 álbume máis" +msgstr[1] "Mostrar %{ count } álbumes máis" + +#: front/src/components/audio/album/Card.vue:40 +msgid "Show 1 more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "Mostar 1 canción máis" +msgstr[1] "Mostrar %{ count } cancións máis" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "Mostrar/ocultar contrasinal" + +#: front/src/components/federation/LibraryFollowTable.vue:97 +#: front/src/components/federation/LibraryTrackTable.vue:98 +#: front/src/components/library/import/BatchDetail.vue:128 +#: front/src/components/library/import/BatchList.vue:73 +#: front/src/components/manage/library/FilesTable.vue:97 +#: front/src/components/manage/library/RequestsTable.vue:104 +#: front/src/components/manage/users/InvitationsTable.vue:76 +#: front/src/components/manage/users/UsersTable.vue:87 +msgid "Showing results %{ start }-%{ end } on %{ total }" +msgstr "Mostrando resultados %{ start }-%{ end } de %{ total }" + +#: front/src/components/audio/Player.vue:269 +msgid "Shuffle your queue" +msgstr "Barallar a cola" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "Rexistro" + +#: front/src/components/manage/users/UsersTable.vue:40 +#: front/src/views/admin/users/UsersDetail.vue:37 +msgid "Sign-up" +msgstr "Rexistrarse" + +#: front/src/components/audio/track/Table.vue:31 +msgid "Simply copy paste the snippet below into a terminal to launch the download." +msgstr "" +"Simplemente copie e pegue o código inferior nun terminal para lanzar a " +"descarga." + +#: front/src/components/library/Track.vue:69 +#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/manage/library/FilesTable.vue:44 +msgid "Size" +msgstr "Tamaño" + +#: front/src/components/library/import/BatchDetail.vue:37 +#: front/src/components/library/import/BatchDetail.vue:72 +msgid "Skipped" +msgstr "Saltado" + +#: front/src/components/requests/Form.vue:3 +msgid "Something's missing in the library? Let us know what you would like to listen!" +msgstr "Falta algo na biblioteca? Fáganos saber que lle gustaría escoitar!" + +#: front/src/components/audio/Search.vue:25 +msgid "Sorry, we did not found any album matching your query" +msgstr "Lamentámolo, non atopamos ningún álbume que coincida coa consulta" + +#: front/src/components/audio/Search.vue:16 +msgid "Sorry, we did not found any artist matching your query" +msgstr "Lamentámolo, non atopamos ningún artista que coincida coa súa consulta" + +#: front/src/components/library/import/BatchDetail.vue:82 +#: front/src/components/library/import/BatchList.vue:38 +#: front/src/components/library/import/TrackImport.vue:17 +msgid "Source" +msgstr "Fonte" + +#: front/src/App.vue:41 +msgid "Source code" +msgstr "Código fonte" + +#: front/src/App.vue:40 +msgid "Source code (%{version})" +msgstr "Código fonte (%{version})" + +#: front/src/components/auth/Profile.vue:20 +#: front/src/components/manage/users/UsersTable.vue:70 +msgid "Staff member" +msgstr "Persoal do equipo" + +#: front/src/components/radios/Button.vue:4 +msgid "Start" +msgstr "Iniciar" + +#: front/src/components/library/import/FileUpload.vue:28 +msgid "Start Upload" +msgstr "Iniciar subida" + +#: front/src/views/admin/Settings.vue:86 +msgid "Statistics" +msgstr "Estatísticas" + +#: front/src/components/federation/LibraryFollowTable.vue:23 +#: front/src/components/federation/LibraryTrackTable.vue:46 +#: front/src/components/library/import/BatchDetail.vue:66 +#: front/src/components/library/import/BatchDetail.vue:83 +#: front/src/components/library/import/BatchList.vue:11 +#: front/src/components/library/import/BatchList.vue:37 +#: front/src/components/library/import/FileUpload.vue:45 +#: front/src/components/manage/library/RequestsTable.vue:24 +#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/manage/users/InvitationsTable.vue:17 +#: front/src/components/manage/users/InvitationsTable.vue:39 +#: front/src/components/manage/users/UsersTable.vue:43 +msgid "Status" +msgstr "Estado" + +#: front/src/components/radios/Button.vue:3 +msgid "Stop" +msgstr "Deter" + +#: front/src/components/Sidebar.vue:159 +msgid "Stop radio" +msgstr "Deter radio" + +#: front/src/components/library/import/FileUpload.vue:32 +msgid "Stop Upload" +msgstr "Deter subida" + +#: front/src/App.vue:9 src/components/requests/Form.vue:17 +msgid "Submit" +msgstr "Enviar" + +#: front/src/components/requests/Form.vue:22 +msgid "Submit another request" +msgstr "Enviar outra petición" + +#: front/src/components/library/import/BatchDetail.vue:26 +#: front/src/components/library/import/BatchList.vue:39 +msgid "Submitted by" +msgstr "Enviada por" + +#: front/src/views/admin/Settings.vue:85 +msgid "Subsonic" +msgstr "Subsonic" + +#: front/src/components/auth/SubsonicTokenForm.vue:2 +msgid "Subsonic API password" +msgstr "Contrasinal API Subsonic" + +#: front/src/components/library/import/BatchDetail.vue:71 +#: front/src/components/library/import/BatchList.vue:16 +#: front/src/components/library/import/FileUpload.vue:56 +msgid "Success" +msgstr "Correcto" + +#: front/src/App.vue:11 +msgid "Suggested choices" +msgstr "Opcións suxeridas" + +#: front/src/components/playlists/Editor.vue:9 +msgid "Syncing changes to server..." +msgstr "Sincronizando cambios co servidor..." + +#: front/src/components/Home.vue:26 +msgid "That's simple: we loved Grooveshark and we want to build something even better." +msgstr "" +"É simple: encantábanos Grooveshark e queremos construír algo aínda mellor." + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "The Beatles, Mickael Jackson…" + +#: front/src/App.vue:59 +msgid "The funkwhale logo was kindly designed and provided by Francis Gading." +msgstr "" +"O logo de funckwhale foi amablemente deseñado e proporcionado por Francis " +"Gading." + +#: front/src/components/Home.vue:124 +msgid "The plaform is free and open-source, you can install it and modify it without worries" +msgstr "" +"A plataforma é libre de código aberto, pode instalala e modificala sin " +"preocupación" + +#: front/src/components/auth/SubsonicTokenForm.vue:4 +msgid "The Subsonic API is not available on this Funkwhale instance." +msgstr "O API Subsonic non está dispoñible en esta instancia Funkwhale." + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "The White Album, Thriller…" + +#: front/src/components/audio/track/Table.vue:30 +msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." +msgstr "" +"Polo momento non hai xeito de descargar directamente múltiples cancións " +"desde funkwhale dentro de un ficheiro ZIP. Porén, pode utilizar ferramentas " +"da liña de comandos como cURL para descargar facilmente unha lista de " +"cancións." + +#: front/src/components/library/import/Main.vue:149 +msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." +msgstr "" +"Esta importación asociarase coa solicitude de música inferior. Ao rematar a " +"importación a solicitude marcarase como tramitada." + +#: front/src/views/federation/LibraryDetail.vue:195 +msgid "This indicate if the remote library granted you access" +msgstr "Esta sinala si a biblioteca remota lle concedeu acceso" + +#: front/src/components/auth/Profile.vue:16 +msgid "This is you!" +msgstr "Este é vostede!" + +#: front/src/components/common/ActionTable.vue:37 +msgid "This may affect a lot of elements, please double check this is really what you want." +msgstr "" +"Esto podería afectar a moitos elementos, por favor comprobe si realmente é o " +"que quere." + +#: front/src/components/audio/PlayButton.vue:61 +msgid "This track is not imported and cannot be played" +msgstr "Esta canción non está importada e non se pode reproducir" + +#: front/src/views/playlists/Detail.vue:37 +msgid "This will completely delete this playlist and cannot be undone." +msgstr "" +"Eliminará completamente a lista de reprodución e non poderá voltar atrás." + +#: front/src/views/radios/Detail.vue:27 +msgid "This will completely delete this radio and cannot be undone." +msgstr "Eliminará completamente a radio e non ten volta atrás." + +#: front/src/components/auth/SubsonicTokenForm.vue:51 +msgid "This will completely disable access to the Subsonic API using from account." +msgstr "Desactivará o acceso a API Subsonic desde a conta." + +#: front/src/App.vue:137 +msgid "This will erase your local data and disconnect you, do you want to continue?" +msgstr "Eliminará os datos locais e será desconectada, desexa continuar?" + +#: front/src/components/auth/SubsonicTokenForm.vue:36 +msgid "This will log you out from existing devices that use the current password." +msgstr "" +"Será desconectada dos dispositivos existentes que utilicen o contrasinal " +"actual." + +#: front/src/components/playlists/Editor.vue:44 +msgid "This will remove all tracks from this playlist and cannot be undone." +msgstr "" +"Esto eliminará todas as cancións da lista de reprodución e non hai volta." + +#: front/src/components/audio/track/Table.vue:6 +#: front/src/components/federation/LibraryTrackTable.vue:47 +#: front/src/components/manage/library/FilesTable.vue:37 +msgid "Title" +msgstr "Título" + +#: front/src/components/audio/SearchBar.vue:27 src/components/library/Track.vue:174 +#: front/src/components/library/import/BatchDetail.vue:84 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "Canción" + +#: front/src/components/library/Track.vue:53 +msgid "Track information" +msgstr "Información da canción" + +#: front/src/components/library/radios/Filter.vue:44 +msgid "Track matching filter" +msgstr "Filtro coincidente da canción" + +#: front/src/components/instance/Stats.vue:54 +msgid "tracks" +msgstr "cancións" + +#: front/src/components/library/Album.vue:43 +#: front/src/components/playlists/PlaylistModal.vue:33 +#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 +#: front/src/views/radios/Detail.vue:34 +msgid "Tracks" +msgstr "Cancións" + +#: front/src/views/federation/LibraryDetail.vue:125 +msgid "Tracks available in this library" +msgstr "Cancións dispoñibles en esta biblioteca" + +#: front/src/components/library/Artist.vue:54 +msgid "Tracks by this artist" +msgstr "Cancións de este artista" + +#: front/src/components/instance/Stats.vue:25 +msgid "Tracks favorited" +msgstr "Cancións favorecidas" + +#: front/src/components/instance/Stats.vue:19 +msgid "tracks listened" +msgstr "cancións escoitadas" + +#: front/src/views/federation/LibraryDetail.vue:109 +msgid "Trigger scan" +msgstr "Iniciar escaneo" + +#: front/src/components/manage/library/FilesTable.vue:41 +msgid "Type" +msgstr "Tipo" + +#: front/src/components/About.vue:15 +msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." +msgstr "" +"Desgraciadamente os donos de esta instancia non tiveron tempo de completar " +"esta páxina." + +#: front/src/views/federation/LibraryDetail.vue:95 +msgid "Unknown" +msgstr "Descoñecido" + +#: front/src/components/Home.vue:37 +msgid "Unlimited music" +msgstr "Música sen límites" + +#: front/src/components/audio/Player.vue:264 +msgid "Unmute" +msgstr "Dar voz" + +#: front/src/components/auth/Settings.vue:50 +msgid "Update avatar" +msgstr "Actualizar avatar" + +#: front/src/components/playlists/Form.vue:33 +msgid "Update playlist" +msgstr "Actualizar lista de reprodución" + +#: front/src/components/auth/Settings.vue:27 +msgid "Update settings" +msgstr "Actualizar axustes" + +#: front/src/views/auth/PasswordResetConfirm.vue:21 +msgid "Update your password" +msgstr "Actualizar contrasinal" + +#: front/src/components/manage/users/UsersTable.vue:172 +#: front/src/views/admin/users/UsersDetail.vue:153 +msgid "Upload" +msgstr "Subir" + +#: front/src/components/auth/Settings.vue:45 +msgid "Upload a new avatar" +msgstr "Subir un novo avatar" + +#: front/src/components/library/import/Main.vue:7 +msgid "Uploaded files or external source" +msgstr "Ficheiros subidos ou fonte externa" + +#: front/src/components/library/import/FileUpload.vue:57 +msgid "Uploading..." +msgstr "Subindo..." + +#: front/src/App.vue:45 +msgid "Use another instance" +msgstr "Utilizar outra instancia" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "" +"Utilice esta caixa de comentarios para engadir detalles da petición si fose " +"preciso" + +#: front/src/views/federation/LibraryDetail.vue:196 +msgid "Use this flag to enable/disable federation with this library" +msgstr "" +"Utilice esta marca para activar/desactivar a federación de esta biblioteca" + +#: front/src/views/auth/PasswordReset.vue:12 +msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." +msgstr "" +"Utilice este formulario para solicitar o restablecemento do contrasinal. " +"Enviarémoslle un correo-e con instrucións para restablecelo." + +#: front/src/components/federation/LibraryForm.vue:6 +msgid "Use this form to scan an instance and setup federation." +msgstr "" +"Utilice este formulario para escanear unha instancia e configurar a " +"federación." + +#: front/src/components/manage/users/InvitationsTable.vue:49 +msgid "Used" +msgstr "Utilizado" + +#: front/src/components/manage/library/RequestsTable.vue:47 +msgid "User" +msgstr "Usuaria" + +#: front/src/components/instance/Stats.vue:5 +msgid "User activity" +msgstr "Actividade da usuaria" + +#: front/src/components/library/Radios.vue:20 +msgid "User radios" +msgstr "Radios da usuaria" + +#: front/src/components/auth/Signup.vue:19 +#: front/src/components/manage/users/UsersTable.vue:37 +msgid "Username" +msgstr "Nome de usuaria" + +#: front/src/components/auth/Login.vue:15 +msgid "Username or email" +msgstr "Nome de usuaria ou correo-e" + +#: front/src/components/instance/Stats.vue:13 +msgid "users" +msgstr "usuarias" + +#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 +#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 +#: front/src/views/admin/users/UsersList.vue:21 +msgid "Users" +msgstr "Usuarias" + +#: front/src/components/library/Album.vue:37 src/components/library/Artist.vue:35 +#: front/src/components/library/Track.vue:44 +#: front/src/components/library/import/ArtistImport.vue:131 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +msgid "View on MusicBrainz" +msgstr "Ver en MusicBrainz" + +#: front/src/components/playlists/PlaylistModal.vue:20 +msgid "We cannot add the track to a playlist" +msgstr "Non podemos engadir a canción a lista de reprodución" + +#: front/src/components/playlists/Form.vue:14 +msgid "We cannot create the playlist" +msgstr "Non podemos crear a lista de reprodución" + +#: front/src/components/auth/Signup.vue:13 +msgid "We cannot create your account" +msgstr "Non podemos crear a súa conta" + +#: front/src/components/auth/Login.vue:7 +msgid "We cannot log you in" +msgstr "Non podemos conectala" + +#: front/src/components/auth/Settings.vue:38 +msgid "We cannot save your avatar" +msgstr "Non podemos gardar o seu avatar" + +#: front/src/components/auth/Settings.vue:14 +msgid "We cannot save your settings" +msgstr "Non podemos gardar os axustes" + +#: front/src/components/Home.vue:130 +msgid "We do not track you or bother you with ads" +msgstr "Nin a perseguimos na internet nin molestamos con publicidade" + +#: front/src/components/library/import/FileUpload.vue:5 +msgid "We recommend using Picard for that purpose." +msgstr "Recomendámoslle utilizar Picard para ese propósito." + +#: front/src/components/Home.vue:7 +msgid "We think listening to music should be simple." +msgstr "Cremos que escoitar música debería ser simple." + +#: front/src/components/PageNotFound.vue:10 +msgid "We're sorry, the page you asked for does not exist:" +msgstr "Lamentámolo, a páxina que solicitou non existe:" + +#: front/src/components/requests/Form.vue:21 +msgid "We've received your request, you'll get some groove soon ;)" +msgstr "Recibimos a súa solicitude, pronto terá boas novas ;)" + +#: front/src/components/Home.vue:152 +msgid "Welcome" +msgstr "Benvida" + +#: front/src/components/Home.vue:5 +msgid "Welcome on Funkwhale" +msgstr "Benvida a Funkwhale" + +#: front/src/components/library/import/Main.vue:114 +msgid "What is metadata?" +msgstr "Qué son os metadatos?" + +#: front/src/views/federation/LibraryDetail.vue:197 +msgid "When enabled, auto importing will automatically import new tracks published in this library" +msgstr "" +"Si está activado, a importación automática importará sen intervención as " +"novas cancións publicadas en esta biblioteca" + +#: front/src/components/Home.vue:24 +msgid "Why funkwhale?" +msgstr "Por qué funkwhale?" + +#: front/src/components/Sidebar.vue:124 +msgid "Yes" +msgstr "Si" + +#: front/src/components/auth/Logout.vue:8 +msgid "Yes, log me out!" +msgstr "Si, desconectádeme!" + +#: front/src/components/auth/Logout.vue:7 +msgid "You are currently logged in as %{ username }" +msgstr "Está conectada como %{ username }" + +#: front/src/components/library/import/Main.vue:111 +msgid "You can also skip this step and enter metadata manually." +msgstr "Pode saltar este paso e introducir os metadatos manualmente." + +#: front/src/components/Home.vue:136 +msgid "You can invite friends and family to your instance so they can enjoy your music" +msgstr "" +"Pode convidar amigos e familiares a súa instancia para que desfruten da súa " +"música" + +#: front/src/components/library/radios/Builder.vue:7 +msgid "You can use this interface to build your own custom radio, which will play tracks according to your criteria." +msgstr "" +"Pode utilizar esta interface para construír a súa propia radio, que " +"reproducirá cancións segundo o seu criterio." + +#: front/src/components/auth/SubsonicTokenForm.vue:8 +msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." +msgstr "" +"Pode utilizalos para desfrutar da súa lista de reprodución e música en modo " +"fora de liña, no seu dispositivo móbil ou tableta, por exemplo." + +#: front/src/components/Sidebar.vue:156 +msgid "You have a radio playing" +msgstr "Ten a radio a funcionar" + +#: front/src/App.vue:6 +msgid "You need to select an instance in order to continue" +msgstr "Debe seleccionar unha instancia para poder continuar" + +#: front/src/components/auth/Settings.vue:100 +msgid "You will be logged out from this session and have to log in with the new one" +msgstr "Será desconectada de esta sesión e deberá conectar co novo" + +#: front/src/components/auth/Settings.vue:71 +msgid "You will have to update your password on your clients that use this password." +msgstr "" +"Deberá actualizar o contrasinal nos seus clientes que utilicen este " +"contrasinal." + +#: front/src/components/library/import/Main.vue:103 +msgid "You will import:" +msgstr "Vai importar:" + +#: front/src/views/auth/EmailConfirm.vue:24 +msgid "Your email address was confirmed, you can now use the service without limitations." +msgstr "" +"O seu enderezo de correo foi confirmado, xa pode utilizar o servizo sen " +"limitacións." + +#: front/src/components/favorites/List.vue:109 +msgid "Your Favorites" +msgstr "As súas Favoritas" + +#: front/src/components/Home.vue:117 +msgid "Your music, your way" +msgstr "A súa música, o seu xeito" + +#: front/src/views/auth/PasswordResetConfirm.vue:29 +msgid "Your password has been updated successfully." +msgstr "O seu contrasinal foi actualizado correctamente." + +#: front/src/components/auth/Settings.vue:101 +msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" +msgstr "" +"O seu contrasinal Subsonic será cambiado por un novo, aleatorio, " +"desconectándoa de todos os dispositivos que utilicen os contrasinal antigo" + +#: front/src/components/audio/PlayButton.vue:156 +msgid "%{ count } track was added to your queue" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "Engadiuse %{ count } canción a cola" +msgstr[1] "Engadíronse %{ count } cancións a cola" diff --git a/front/locales/it/LC_MESSAGES/app.po b/front/locales/it/LC_MESSAGES/app.po index 566e1d6ac..5adb8c990 100644 --- a/front/locales/it/LC_MESSAGES/app.po +++ b/front/locales/it/LC_MESSAGES/app.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: front 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-22 14:12+0200\n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" "PO-Revision-Date: 2018-08-06 12:12+0000\n" "Last-Translator: Sylke Vicious \n" "Language-Team: none\n" @@ -30,15 +30,16 @@ msgstr "(%{ index } su %{ length })" msgid "(empty)" msgstr "(vuoto)" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "%{ count } su %{ total } selezionato" msgstr[1] "%{ count } su %{ total } selezionati" -#: front/src/components/Sidebar.vue:116 -#: src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 +#: src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" msgstr[0] "%{ count } traccia" @@ -50,7 +51,7 @@ msgid_plural "%{ count } tracks in %{ albumsCount } albums" msgstr[0] "%{ count } traccia in %{ albumsCount } album" msgstr[1] "%{ count } tracce in %{ albumsCount } album" -#: front/src/components/library/radios/Builder.vue:76 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" msgstr[0] "%{ count } traccia corrisponde ai filtri combinati" @@ -62,6 +63,11 @@ msgid_plural "%{ count } tracks" msgstr[0] "%{ count} traccia" msgstr[1] "%{ count} tracce" +#: front/src/views/content/libraries/Quota.vue:11 +#, fuzzy +msgid "%{ current } used on %{ max } allowed" +msgstr "%{ count } su %{ total } selezionato" + #: front/src/components/common/Duration.vue:2 msgid "%{ hours } h %{ minutes } min" msgstr "%{ hours } o %{ minutes } min" @@ -78,10 +84,6 @@ msgstr "a %{ user } piace una traccia" msgid "%{ user } listened to a track" msgstr "%{ user } ha ascoltato una traccia" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "Profilo di %{ username }" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" @@ -94,35 +96,28 @@ msgid_plural "%{ count } favorites" msgstr[0] "1 mi piace" msgstr[1] "%{ count } mi piace" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "1 traccia" -msgstr[1] "%{ count } tracce" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "A proposito di %{ instance }" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "A proposito di Funkwhale" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "A proposito di questa istanza" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 +#: front/src/views/content/libraries/Detail.vue:48 +#, fuzzy +msgid "Accept" +msgstr "Accettato" + +#: front/src/views/content/libraries/Detail.vue:40 msgid "Accepted" msgstr "Accettato" -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "Accesso disabilitato" - -#: front/src/components/Home.vue:109 +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" msgstr "Accedi alla tua musica da un'interfaccia pulita che si focalizza su quello che conta davvero" @@ -134,10 +129,6 @@ msgstr "Account attivo" msgid "Account settings" msgstr "Impostazioni dell'account" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "Impostazioni dell'account" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "Stato dell'account" @@ -146,16 +137,19 @@ msgstr "Stato dell'account" msgid "Account's email" msgstr "Email dell'account" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +#, fuzzy +msgid "Action" +msgstr "Azioni" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" msgstr[0] "L'azione %{ action } è stata lanciata con successo su %{ count } elemento" msgstr[1] "L'azione %{ action } è stata lanciata con successo su %{ count } elementi" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:60 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "Azioni" @@ -163,30 +157,22 @@ msgstr "Azioni" msgid "Active" msgstr "Attivo" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "Attività" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" -msgstr "Attore" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" +msgstr "" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "Aggiungi una nuova libreria" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" msgstr "Aggiungi filtro" -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" msgstr "Aggiungi filtri per personalizzare la tua radio" -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "Aggiungi alla coda corrente" - #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" @@ -201,10 +187,6 @@ msgstr "Aggiungi alla playlist..." msgid "Add to queue" msgstr "Aggiungi alla coda" -#: front/src/components/playlists/PlaylistModal.vue:116 -msgid "Add to this playlist" -msgstr "Aggiungi a questa playlist" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "Aggiungi traccia" @@ -213,25 +195,19 @@ msgstr "Aggiungi traccia" msgid "Admin" msgstr "Amministratore" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" msgstr "Amministrazione" -#: front/src/components/audio/SearchBar.vue:26 -#: src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 +#: src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "Album" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "Album %{ title } (%{ count } traccia) di %{ artist }" -msgstr[1] "Album %{ title } (%{ count } tracce) di %{ artist }" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" @@ -244,7 +220,6 @@ msgstr "Pagina dell'album" #: front/src/components/audio/Search.vue:19 #: src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "Albums" @@ -253,8 +228,8 @@ msgstr "Albums" msgid "Albums by this artist" msgstr "Albums di questo artista" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "Tutto" @@ -264,49 +239,23 @@ msgstr "C'è stato un errore durante il salvataggio delle tue modifiche" #: front/src/components/auth/Login.vue:10 msgid "An unknown error happend, this can mean the server is down or cannot be reached" -msgstr "" -"Si è verificato un errore sconosciuto, questo significa che il server è " -"offline o non può essere raggiunto" - -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "Qualsiasi" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "API" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "Approva" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "Approvi l'accesso?" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "Approvato" +msgstr "Si è verificato un errore sconosciuto, questo significa che il server è offline o non può essere raggiunto" #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "Sei sicuro di volerti disconnettere?" -#: front/src/components/audio/SearchBar.vue:25 -#: src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 +#: src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" msgstr "Artista" #: front/src/components/requests/Form.vue:5 +#: src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "Nome dell'artista" @@ -315,26 +264,20 @@ msgstr "Nome dell'artista" msgid "Artist page" msgstr "Pagina dell'artista" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "Artista, album, traccia..." - #: front/src/components/audio/Search.vue:10 #: src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 -#: src/components/library/Library.vue:7 +#: front/src/components/library/Library.vue:7 +#: src/components/library/Artists.vue:120 msgid "Artists" msgstr "Artisti" #: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 -#: src/components/library/Radios.vue:44 +#: src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 -#: src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" msgstr "Crescente" @@ -342,10 +285,6 @@ msgstr "Crescente" msgid "Ask for a password reset" msgstr "Chiedi un reset della password" -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "Auto importazione" - #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" msgstr "Playlist disponibili" @@ -359,12 +298,9 @@ msgstr "Avatar" msgid "Back to login" msgstr "Torna alla pagina di accesso" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "Fai attenzione quando accetti le richieste di seguirti, perchè significa che quell'utente avrà accesso a tutta la tua libreria." - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "Bitrate" @@ -372,7 +308,7 @@ msgstr "Bitrate" msgid "Browse" msgstr "Sfoglia" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "Sfoglia libreria" @@ -380,18 +316,6 @@ msgstr "Sfoglia libreria" msgid "Browsing artists" msgstr "Sfogliando artisti" -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "Sfogliando tracce federate" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "Sfogliando followers" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "Sfogliando librerie" - #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" msgstr "Sfogliando playlists" @@ -408,22 +332,17 @@ msgstr "Crea" msgid "By %{ artist }" msgstr "Di %{ artist }" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." -msgstr "Confermando, a %{ username } sarà negato l'accesso alla tua libreria." +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." +msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "Confermando, a %{ username } sarà consentito l'accesso alla tua libreria." - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" msgstr "Annulla" -#: front/src/components/library/radios/Builder.vue:59 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "Candidati" @@ -431,7 +350,7 @@ msgstr "Candidati" msgid "Cannot change your password" msgstr "Non puoi cambiare la tua password" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" msgstr "Cambia lingua" @@ -464,7 +383,7 @@ msgstr "Cambiando la tua password cambierà anche la password della API Subsonic msgid "Changing your password will have the following consequences" msgstr "Cambiare la tua password avrà queste conseguenze" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "Scegli la tua istanza" @@ -481,22 +400,13 @@ msgstr "Pulisci" msgid "Clear playlist" msgstr "Pulisci playlist" -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "Pulisci la tua coda" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "CLI" - #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" msgstr "Clicca una volta, ascolta per ore utilizzando le radio integrate" -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" -msgstr "Chiuso" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" +msgstr "" #: front/src/components/manage/users/InvitationForm.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:42 @@ -509,12 +419,11 @@ msgstr "Codice" msgid "Collapse" msgstr "Riduci" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "Commenta" -#: front/src/components/library/radios/Builder.vue:58 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" msgstr "Configurazione" @@ -531,11 +440,11 @@ msgstr "Conferma la tua email" msgid "Confirmation code" msgstr "Codice di conferma" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" -msgstr "Copia tracce dalla tua coda corrente alla playlist" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" +msgstr "" -#: front/src/components/Home.vue:88 +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" msgstr "Copertine, testi, il nostro obbiettivo è averli tutti ;)" @@ -543,19 +452,28 @@ msgstr "Copertine, testi, il nostro obbiettivo è averli tutti ;)" msgid "Create a funkwhale account" msgstr "Crea un account funkwhale" +#: front/src/views/content/libraries/Home.vue:14 +#, fuzzy +msgid "Create a new library" +msgstr "Crea una nuova playlist" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "Crea una nuova playlist" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "Crea un account" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "Crea importazione" +#: front/src/views/content/libraries/Form.vue:26 +#, fuzzy +msgid "Create library" +msgstr "Pulisci libreria" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "Crea il mio account" @@ -568,9 +486,8 @@ msgstr "Crea playlist" msgid "Create your own radio" msgstr "Crea la tua radio" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "Data di creazione" @@ -578,17 +495,37 @@ msgstr "Data di creazione" msgid "Current avatar" msgstr "Avatar attuale" +#: front/src/views/content/libraries/DetailArea.vue:4 +#, fuzzy +msgid "Current library" +msgstr "Pulisci libreria" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "Traccia corrente" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +#, fuzzy +msgid "Current usage" +msgstr "Traccia corrente" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:29 +#: src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "Elimina" +#: front/src/views/content/libraries/Form.vue:39 +#, fuzzy +msgid "Delete library" +msgstr "Pulisci libreria" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "Elimina playlist" @@ -597,36 +534,35 @@ msgstr "Elimina playlist" msgid "Delete radio" msgstr "Elimina radio" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" -msgstr "Nega" - -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "Negare accesso?" +#: front/src/views/content/libraries/Form.vue:31 +#, fuzzy +msgid "Delete this library?" +msgstr "Portami alla libreria" #: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 -#: src/components/library/Radios.vue:47 +#: src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 -#: src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "Decrescente" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +#, fuzzy +msgid "Description" +msgstr "Durata" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "Dettaglio" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." -msgstr "" -"Determina se un account utente è attivo o meno. Gli utenti inattivi non " -"possono accedere o utilizzare il servizio." +#: front/src/views/content/remote/Card.vue:50 +#, fuzzy +msgid "Details" +msgstr "Dettaglio" #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 @@ -645,7 +581,7 @@ msgstr "Disabilitare l'accesso alle API Subsonic?" msgid "Discover how to use Funkwhale from other apps" msgstr "Scopri come utilizzare Funkwhale attraverso altre app" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "Mostra pubblicamente" @@ -671,41 +607,44 @@ msgid_plural "Do you want to launch %{ action } on %{ count } elements?" msgstr[0] "Vuoi eseguire %{ action } su %{ count } elemento?" msgstr[1] "Vuoi eseguire %{ action } su %{ count } elementi?" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "Vuoi ripristinare la tua coda precedente?" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "Documentazione" -#: front/src/components/audio/track/Table.vue:24 -#: src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "Scarica" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "Scarica tracce" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" msgstr "Trascina e rilascia righe per riordinare le tracce nella playlist" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 +#: src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "Durata" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "Facile da utilizzare" +#: front/src/views/content/libraries/Detail.vue:9 +#, fuzzy +msgid "Edit" +msgstr "Modifica..." + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "Modifica le info dell'istanza" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "Modifica..." @@ -726,55 +665,17 @@ msgstr "Email confermata" msgid "End edition" msgstr "Fine modifica" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "Assicurati che i tuoi files musicali siano taggati in modo corretto prima di caricarli." - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "Inserisci un nome di una radio..." - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "Inserisci il nome di un artista..." - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "Inserisci il nome del dominio di una libreria..." - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "Inserisci il nome di una playlist..." - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "Inserisci la tua email" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "Inserisci il tuo codice di invito (non tiene conto di maiuscole o minuscole)" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "Inserisci i tuoi criteri di ricerca..." - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "Inserisci il tuo nome utente" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "Inserisci il tuo nome utente o l'email" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "Errore" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" +#: front/src/views/content/remote/Card.vue:39 +#, fuzzy +msgid "Error during scan" msgstr "Segnalazione errore" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "Errore durante l'esecuzione dell'azione" @@ -794,29 +695,31 @@ msgstr "Errore durante la conferma della tua email" msgid "Error while creating invitation" msgstr "Errore durante la creazione dell'invito" +#: front/src/views/content/remote/ScanForm.vue:3 +#, fuzzy +msgid "Error while fetching remote library" +msgstr "Errore durante la scansione della libreria" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "Errore durante il salvataggio delle impostazioni" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "Errore durante la scansione della libreria" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "Si è verificato un errore" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" -msgstr "Tutti" +#: front/src/views/content/libraries/Quota.vue:75 +#, fuzzy +msgid "Errored files" +msgstr "Si è verificato un errore" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" -msgstr "Tutti su questa istanza" +#: front/src/views/content/remote/Card.vue:58 +#, fuzzy +msgid "Errored tracks:" +msgstr "Tracce federate" -#: front/src/components/library/radios/Builder.vue:57 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "Escludi" @@ -825,6 +728,7 @@ msgid "Expand" msgstr "Espandi" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "Data di scadenza" @@ -836,97 +740,51 @@ msgstr "Scaduto" msgid "Expired/used" msgstr "Scaduto/utilizzato" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "Sorgente esterna. Supporta backends" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "Preferiti" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "Unisciti ad una nuova istanza" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "Tracce federate" - -#: front/src/components/Sidebar.vue:87 -#: src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 -#: src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 -#: src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "Federazione" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "Mirroring dei file" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "Nome file" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "Carica file" - #: front/src/views/admin/library/Base.vue:5 #: src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "Files" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "Filtra tipi di album" - -#: front/src/components/library/radios/Builder.vue:56 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "Filtra nome" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "Termina importazione" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "Finito" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "Prima, scegli da dove vuoi importare la musica" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "Segui" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" +#: front/src/views/content/remote/Card.vue:88 +#, fuzzy +msgid "Follow pending approval" msgstr "Richiesta di seguire in attesa di approvazione" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" -msgstr "Stato di segui" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" +msgstr "" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "Seguito da" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "Solo chi segue" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "Seguendo" -#: front/src/components/activity/Like.vue:12 -#: src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 +#: src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "da %{ album } di %{ artist }" @@ -934,30 +792,23 @@ msgstr "da %{ album } di %{ artist }" msgid "From album %{ album } by %{ artist }" msgstr "Dall'album %{ album } di %{ artist }" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" -msgstr "" -"Funkwhale è un progetto libero e open-source portato avanti da volontari. " -"Puoi aiutarci a migliorare la piattaforma segnalando problemi, suggerendo " -"miglioramenti e condividendo il progetto con i tuoi amici!" +msgstr "Funkwhale è un progetto libero e open-source portato avanti da volontari. Puoi aiutarci a migliorare la piattaforma segnalando problemi, suggerendo miglioramenti e condividendo il progetto con i tuoi amici!" #: front/src/components/auth/SubsonicTokenForm.vue:7 msgid "Funkwhale is compatible with other music players that support the Subsonic API." -msgstr "" -"Funkwhale è compatibile con altri lettori musicali che supportano le " -"Subsonic API." +msgstr "Funkwhale è compatibile con altri lettori musicali che supportano le Subsonic API." -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "Funkwhale è semplicissimo da usare." #: front/src/components/Home.vue:39 msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." -msgstr "" -"Funkwhale è progettato per permetterti di ascoltare facilmente la musica che " -"ti piace, o per farti scoprire nuovi artisti." +msgstr "Funkwhale è progettato per permetterti di ascoltare facilmente la musica che ti piace, o per farti scoprire nuovi artisti." -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." msgstr "Funkwhale è gratuito e ti da il controllo sulla tua musica." @@ -973,18 +824,20 @@ msgstr "Ottieni un nuovo invito" msgid "Get me to the library" msgstr "Portami alla libreria" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " +#: front/src/components/Home.vue:76 +#, fuzzy +msgid "Get quality metadata about your music thanks to MusicBrainz" msgstr "" "Ottieni metadati di qualità sulla tua musica grazie a\n" " \n" " MusicBrainz\n" " " +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +#, fuzzy +msgid "Get started" +msgstr "Prossimo passo" + #: front/src/components/common/ActionTable.vue:21 #: front/src/components/common/ActionTable.vue:27 msgid "Go" @@ -994,175 +847,58 @@ msgstr "Vai" msgid "Go to home page" msgstr "Vai alla pagina iniziale" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "Ottieni metadata corrispondenti" - -#: front/src/App.vue:74 +#: front/src/App.vue:73 msgid "Help us translate Funkwhale" msgstr "Aiutaci a tradurre Funkwhale" -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "Pagina Iniziale" - #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" msgstr "Ore di musica" #: front/src/components/auth/SubsonicTokenForm.vue:11 msgid "However, accessing Funkwhale from those clients require a separate password you can set below." -msgstr "" -"Comunque, accedere a Funkwhale da quei client richiede un'altra password che " -"puoi impostare qui sotto." - -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "ID" +msgstr "Comunque, accedere a Funkwhale da quei client richiede un'altra password che puoi impostare qui sotto." #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." -msgstr "" -"Se l'indirizzo email fornito nel passo precedente è valido e legato ad un " -"account utente, dovresti ricevere un'email con le istruzioni per il reset " -"nel prossimo paio di minuti." - -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "Importa" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "Importazione #%{ id } lanciata" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "Importa %{ count } traccia" -msgstr[1] "Importa %{ count } tracce" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "Importa batch" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "Importa Batch #%{ id }" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "Importa più batch" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "Importa Batch" +msgstr "Se l'indirizzo email fornito nel passo precedente è valido e legato ad un account utente, dovresti ricevere un'email con le istruzioni per il reset nel prossimo paio di minuti." #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "Importa data" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "Pagina dei dettagli di importazione" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "Importa musica" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "Importa Musica" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" msgstr "Importa musica da varie piattaforme, come YouTube o SoundCloud" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" -msgstr "Importazioni in attesa" - -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "Importazioni richieste" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" +#: front/src/components/library/FileUpload.vue:51 +#, fuzzy +msgid "Import reference" msgstr "Importa sorgente" -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "Stato dell'importazione" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "Importa questa versione" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "Importa questa traccia" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "Importato" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "URL importato" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "Importazioni" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "Nei preferiti" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "In libreria" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "Inattivo" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "Inserisci un ID MusicBrainz manualmente:" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "Inserisci l'indirizzo email associato al tuo account" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" msgstr[0] "Inserita dalla coda (%{ count } traccia)" msgstr[1] "Inserite dalla coda (%{ count } tracce)" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "Informazioni sull'istanza" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "Radio dell'istanza" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "Impostazioni dell'istanza" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "Timeline dell'Istanza" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1178,50 +914,43 @@ msgstr "Codice d'invito (opzionale)" msgid "Invitations" msgstr "Inviti" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "Elenco problemi" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "ID Lavoro" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" -msgstr "Lavori" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" +msgstr "" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" msgstr "Tieni d'occhio le tue tracce preferite" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." -msgstr "" -"Tieni i tuoi PRIVATE_TOKEN segreti visto che danno accesso al tuo account." +#: front/src/views/content/remote/Home.vue:14 +#, fuzzy +msgid "Known libraries" +msgstr "Sfogliando librerie" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "Ultime attività" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "Ultime recuperate" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "Ultima modifica" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +#, fuzzy +msgid "Last update:" +msgstr "Lista di riproduzione aggiornata" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "Inizia" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "Data d'inizio" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" msgstr "Inizia scansione" @@ -1229,25 +958,21 @@ msgstr "Inizia scansione" msgid "Learn more about this instance" msgstr "Scopri di più su questa istanza" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "Lascia vuoto per un codice casuale" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "Lascia questo campo vuoto se vuoi richiedere la discografia completa." -#: front/src/views/federation/Base.vue:5 -#: src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "Librerie" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "" + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "Libreria" @@ -1255,159 +980,119 @@ msgstr "Libreria" msgid "Library files" msgstr "File della libreria" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "Nome della libreria" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "Dimensione della libreria" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "library@demo.funkwhale.audio" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "Collegamenti" +#: front/src/views/content/libraries/Detail.vue:21 +#, fuzzy +msgid "Loading followers..." +msgstr "Sfogliando followers" + +#: front/src/views/content/libraries/Home.vue:3 +#, fuzzy +msgid "Loading Libraries..." +msgstr "Caricando i tuoi preferiti..." + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +#, fuzzy +msgid "Loading library data..." +msgstr "Inserisci il nome del dominio di una libreria..." + +#: front/src/views/Notifications.vue:4 +#, fuzzy +msgid "Loading notifications..." +msgstr "Caricando la timeline..." + +#: front/src/views/content/remote/Home.vue:3 +#, fuzzy +msgid "Loading remote libraries..." +msgstr "Caricando la timeline..." + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "Caricando la timeline..." +#: front/src/views/content/libraries/Quota.vue:4 +#, fuzzy +msgid "Loading usage data..." +msgstr "Caricando i tuoi preferiti..." + #: front/src/components/favorites/List.vue:5 msgid "Loading your favorites..." msgstr "Caricando i tuoi preferiti..." -#: front/src/components/auth/Login.vue:78 -msgid "Log In" -msgstr "Accedi" - #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "Accedi al tuo account Funkwhale" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "Disconnetti" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "Accesso effettuato come %{ username }" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "Accedi" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "Disconnettiti" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "" -"Ripetizione disattivata. Clicca per attivare la ripetizione della singola " -"traccia." - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "Ripeti una singola traccia. Clicca per ripetere l'intera coda." - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "Ripete l'intera coda. Clicca per disattivare la ripetizione." - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "Testi" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "Gestisci libreria" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "Gestisci playlist" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "Gestisci utenti" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "Gestisci le tue playlist" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" -msgstr "Segna come chiuso" - -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" +#: front/src/views/Notifications.vue:17 +#, fuzzy +msgid "Mark all as read" msgstr "Segna come importato" -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "Metadati" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" msgstr "" -"I metadata sono i dati relativi alla musica che vuoi importare. Questo " -"include tutte le informazioni su artisti, album e tracce. Per far si che tu " -"abbia una libreria di alta qualità, è consigliato ottenere i dati dal " -"progetto \n" -" \n" -" MusicBrainz\n" -" \n" -" che praticamente è la Wikipedia della musica." -#: front/src/components/Sidebar.vue:48 -#: src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "Musica" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "Richiesta di musica" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "Muto" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "Mio account" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" -msgstr "La mia eccezionale playlist" +#: front/src/views/content/libraries/Home.vue:6 +#, fuzzy +msgid "My libraries" +msgstr "Librerie" -#: front/src/components/library/radios/Builder.vue:227 -msgid "My awesome radio" -msgstr "La mia eccezionale radio" - -#: front/src/components/library/Track.vue:64 -#: src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 +#: src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 +#: src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "N/D" #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "Nome" @@ -1416,48 +1101,41 @@ msgstr "Nome" msgid "New password" msgstr "Nuova password" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "Le nuove tracce saranno allegate qui automaticamente." -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "Prossimo passo" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "Prossima traccia" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "No" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" msgstr "Nessun addon, nessun plugin: ti serve solo una libreria in rete" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "Nessun testo disponibile per questa traccia." -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" -msgstr "Nessuno tranne me" +#: front/src/components/federation/LibraryWidget.vue:6 +#, fuzzy +msgid "No matching library." +msgstr "Inserisci il nome del dominio di una libreria..." -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "Non seguito" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" -msgstr "Non importato" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" +msgstr "" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "Non utilizzato" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +#, fuzzy +msgid "Notifications" +msgstr "Ultima modifica" + +#: front/src/App.vue:36 msgid "Official website" msgstr "Sito ufficiale" @@ -1465,47 +1143,32 @@ msgstr "Sito ufficiale" msgid "Old password" msgstr "Vecchia password" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "" -"Una volta che tutti i tuoi file sono caricati, clicca semplicemene il " -"pulsante seguente per controllare lo stato dell'importazione." - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 msgid "Open" msgstr "Aperto" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "Opzioni" -#: front/src/components/library/import/Main.vue:93 -msgid "Or" -msgstr "O" - #: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 -#: src/components/library/Radios.vue:33 +#: src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 -#: src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" msgstr "Ordinamento" #: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 -#: src/components/library/Radios.vue:41 +#: src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 -#: src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "Direzione di ordinamento" @@ -1513,10 +1176,6 @@ msgstr "Direzione di ordinamento" msgid "Owner" msgstr "Proprietario" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "Pagina Non Trovata" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "Pagina non trovata!" @@ -1525,42 +1184,26 @@ msgstr "Pagina non trovata!" msgid "Password" msgstr "Password" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "Password aggiornata" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "Password aggiornata con successo" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "Metti in pausa" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "In sospeso" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" msgstr "Approvazione in sospeso" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" -msgstr "Richiesta di seguire in sospeso" +#: front/src/views/content/libraries/Quota.vue:22 +#, fuzzy +msgid "Pending files" +msgstr "In sospeso" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "Richieste di importazione in sospeso" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "Richieste in sospeso" @@ -1584,10 +1227,6 @@ msgstr "Riproduci tutto" msgid "Play all albums" msgstr "Riproduci tutti gli album" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "Riproduci immediatamente" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "Riproduci la prossima" @@ -1596,14 +1235,6 @@ msgstr "Riproduci la prossima" msgid "Play now" msgstr "Riproduci ora" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "Riproduci traccia" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "Lista di riproduzione" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1630,9 +1261,9 @@ msgstr "Lista di riproduzione aggiornata" msgid "Playlist visibility" msgstr "Visibilità lista di riproduzione" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" msgstr "Liste di riproduzione" @@ -1652,43 +1283,45 @@ msgstr "Per favore controlla se la combinazione nome utente/password è corretta msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." msgstr "PNG, GIF or JPG. Massimo 2MB. Saranno scalate a 400x400px." -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "Passo precedente" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" -msgstr "Traccia precedente" +#: front/src/components/library/FileUpload.vue:58 +#, fuzzy +msgid "Proceed" +msgstr "Procedi all'accesso" #: front/src/views/auth/EmailConfirm.vue:26 #: front/src/views/auth/PasswordResetConfirm.vue:31 msgid "Proceed to login" msgstr "Procedi all'accesso" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" -msgstr "Data di pubblicazione" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" +msgstr "" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" -msgstr "Modello di ricerca" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" +msgstr "" #: front/src/components/Sidebar.vue:20 msgid "Queue" msgstr "Coda" -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "Coda mischiata!" - -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "Radio" - -#: front/src/components/library/radios/Builder.vue:226 -msgid "Radio Builder" -msgstr "Creatore Radio" - #: front/src/components/library/radios/Builder.vue:15 msgid "Radio created" msgstr "Radio creata" @@ -1702,7 +1335,7 @@ msgid "Radio updated" msgstr "Radio aggiornata" #: front/src/components/library/Library.vue:10 -#: src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 msgid "Radios" msgstr "Radio" @@ -1722,33 +1355,40 @@ msgstr "Preferiti recenti" msgid "Recently listened" msgstr "Ascoltate recentemente" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "Registrazione MusicBrainzID" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" msgstr "Aggiorna" -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "Rifiutato" - #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" msgstr "Registrato da %{ date }" #: front/src/components/auth/Signup.vue:9 msgid "Registration are closed on this instance, you will need an invitation code to signup." -msgstr "" -"Le registrazioni sono chiuse su questa istanza, hai bisogno di un codice " -"d'invito per registrarti." +msgstr "Le registrazioni sono chiuse su questa istanza, hai bisogno di un codice d'invito per registrarti." #: front/src/components/manage/users/UsersTable.vue:71 msgid "regular user" msgstr "utente semplice" +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "" + +#: front/src/views/content/remote/Home.vue:6 +#, fuzzy +msgid "Remote libraries" +msgstr "Portami alla libreria" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." +msgstr "" + #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" msgstr "Rimuovi" @@ -1757,10 +1397,6 @@ msgstr "Rimuovi" msgid "Remove avatar" msgstr "Rimuovi avatar" -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "Rimuovi dai preferiti" - #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" @@ -1774,109 +1410,64 @@ msgstr "Richiedere una nuova password API Subsonic?" msgid "Request a password" msgstr "Richiedi una password" -#: front/src/App.vue:35 -msgid "Request music" -msgstr "Richiedi musica" - -#: front/src/views/library/MusicRequest.vue:4 -#: src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "Richiedi della musica" - #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" msgstr "Richiesta inviata!" -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "Riavvia attività con errori" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "Riavvia attività" - #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" msgstr "Resetta la tua password" -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "Risultato %{ current }/%{ total }" - #: front/src/components/favorites/List.vue:38 #: src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 -#: src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" msgstr "Risultati per pagina" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:29 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" msgstr "Salva" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" -msgstr "Scansione avviata!" +#: front/src/views/content/remote/Card.vue:31 +#, fuzzy +msgid "Scan pending" +msgstr "Crescente" + +#: front/src/views/content/remote/Card.vue:43 +#, fuzzy +msgid "Scanned successfully" +msgstr "Impostazioni aggiornate con successo." + +#: front/src/views/content/remote/Card.vue:47 +#, fuzzy +msgid "Scanned with errors" +msgstr "Modifiche sincronizzate con il server" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" +msgstr "" -#: front/src/components/federation/LibraryTrackTable.vue:5 #: front/src/components/library/Artists.vue:10 #: src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:5 #: src/views/playlists/List.vue:13 msgid "Search" msgstr "Cerca" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "Cerca un elemento che vuoi importare:" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "Cerca per artista, nome utente, commento..." - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "Cerca per sorgente..." - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "Cerca per utente che ha caricato, sorgente..." - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "Cerca per titolo, artista, dominio..." - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "Cerca per nome utente, dominio..." - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "Cerca per nome utente, email, codice..." - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "Cerca per nome utente, email, nome..." - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." -msgstr "Cerca per artisti, album, tracce..." +#: front/src/views/content/remote/ScanForm.vue:9 +#, fuzzy +msgid "Search a remote library" +msgstr "Portami alla libreria" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" msgstr "Cerca un po' di musica" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" msgstr "Cerca su lyrics.wikia.com" @@ -1886,43 +1477,27 @@ msgstr "Cerca su lyrics.wikia.com" msgid "Search on Wikipedia" msgstr "Cerca su Wikipedia" -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "Stringa di ricerca" - #: front/src/views/admin/Settings.vue:15 msgid "Sections" msgstr "Sezioni" -#: front/src/components/library/radios/Builder.vue:41 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" msgstr "Seleziona un filtro" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" msgstr[0] "Seleziona tutti %{ total } elemento" msgstr[1] "Seleziona tutti %{ total } elementi" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "Seleziona file da caricare..." - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" msgstr "Seleziona solo la pagina attuale" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "Seleziona sorgenti adeguate o file da importare" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "Invia una richiesta di seguire" - -#: front/src/components/Sidebar.vue:97 -#: src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" msgstr "Impostazioni" @@ -1934,101 +1509,89 @@ msgstr "Impostazioni aggiornate" msgid "Settings updated successfully." msgstr "Impostazioni aggiornate con successo." -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "Impostazioni..." - #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" msgstr "Condividi collegamento" +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." +msgstr "" + +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +#, fuzzy +msgid "Sharing link" +msgstr "Condividi collegamento" + +#: front/src/components/audio/album/Card.vue:40 +#, fuzzy +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "%{ count } traccia" +msgstr[1] "%{ count } tracce" + #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" msgstr[0] "Mostra 1 altro album" msgstr[1] "Mostra %{ count } altri album" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "Mostra 1 altra traccia" -msgstr[1] "Mostra %{ count } altre tracce" +#: front/src/views/Notifications.vue:10 +#, fuzzy +msgid "Show read notifications" +msgstr "Ultima modifica" -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" -msgstr "Mostra/nascondi password" - -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" msgstr "Mostrando risultati %{ start }-%{ end } su %{ total }" -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "Mischia la tua coda" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "Registrati" - #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" msgstr "Registrati" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "" -"Copia e incolla il codice sottostante in un terminale per iniziare lo " -"scaricamento." - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 +#: src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" msgstr "Dimensione" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 msgid "Skipped" msgstr "Saltato" +#: front/src/views/content/libraries/Quota.vue:49 +#, fuzzy +msgid "Skipped files" +msgstr "Saltato" + #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "Manca qualcosa nella libreria? Facci sapere cosa vorresti ascoltare!" #: front/src/components/audio/Search.vue:25 msgid "Sorry, we did not found any album matching your query" -msgstr "" -"Ci dispiace, non abbiamo trovato nessun album corrispondente alla tua ricerca" +msgstr "Ci dispiace, non abbiamo trovato nessun album corrispondente alla tua ricerca" #: front/src/components/audio/Search.vue:16 msgid "Sorry, we did not found any artist matching your query" -msgstr "" -"Ci dispiace, non abbiamo trovato nessun artista corrispondente alla tua " -"ricerca" +msgstr "Ci dispiace, non abbiamo trovato nessun artista corrispondente alla tua ricerca" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "Sorgente" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "Codice sorgente" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "Codice sorgente (%{version})" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" msgstr "Membro dello staff" @@ -2037,26 +1600,11 @@ msgstr "Membro dello staff" msgid "Start" msgstr "Inizia" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "Inizia caricamento" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "Statistiche" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "Stato" @@ -2064,15 +1612,11 @@ msgstr "Stato" msgid "Stop" msgstr "Ferma" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" msgstr "Ferma radio" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "Ferma Caricamento" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" msgstr "Invia" @@ -2080,99 +1624,85 @@ msgstr "Invia" msgid "Submit another request" msgstr "Invia un'altra richiesta" -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "Inviato da" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "Subsonic" - #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" msgstr "Password API Subsonic" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" -msgstr "Successo" - -#: front/src/App.vue:11 +#: front/src/App.vue:13 msgid "Suggested choices" msgstr "Scelte suggerite" +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" +msgstr "" + #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "Sincronizzando le modifiche con il server..." +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." -msgstr "" -"È molto semplice: amavamo Grooveshark e volevamo creare qualcosa ancora più " -"bello." +msgstr "È molto semplice: amavamo Grooveshark e volevamo creare qualcosa ancora più bello." -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "The Beatles, Michael Jackson…" - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." -msgstr "" -"Il logo di Funkwhale è stato gentilmente disegnato e concesso da Francis " -"Gading." +msgstr "Il logo di Funkwhale è stato gentilmente disegnato e concesso da Francis Gading." -#: front/src/components/Home.vue:124 -msgid "The plaform is free and open-source, you can install it and modify it without worries" +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." msgstr "" -"La piattaforma è libera e open source, puoi installarla e modificarla senza " -"problemi" + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "" + +#: front/src/components/Home.vue:121 +msgid "The plaform is free and open-source, you can install it and modify it without worries" +msgstr "La piattaforma è libera e open source, puoi installarla e modificarla senza problemi" #: front/src/components/auth/SubsonicTokenForm.vue:4 msgid "The Subsonic API is not available on this Funkwhale instance." msgstr "L'API Subsonic non è disponibile su questa istanza Funkwhale." -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" -msgstr "The White Album, Thriller…" - -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" msgstr "" -"Attualmente non c'è modo di scaricare direttamente più tracce da Funkwhale " -"come archivio ZIP. Comunque, puoi usare uno strumento da terminale come cURL " -"per scaricare facilmente una lista di tracce." -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" msgstr "" -"Questa importazione sarà associata alla richiesta di musica sottostante. Una " -"volta che l'importazione è finita, la richiesta verrà segnata come " -"completata." -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" -msgstr "Questo indica se la libreria remota ti ha concesso l'accesso" +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" +msgstr "" + +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." +msgstr "" #: front/src/components/auth/Profile.vue:16 msgid "This is you!" msgstr "Questo sei tu!" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." -msgstr "" -"Questo può coinvolgere molti elementi, per favore ricontrolla se è proprio " -"quello che vuoi." +msgstr "Questo può coinvolgere molti elementi, per favore ricontrolla se è proprio quello che vuoi." -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" -msgstr "Questa traccia non è importata e non può essere riprodotta" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "" + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" +msgstr "" #: front/src/views/playlists/Detail.vue:37 msgid "This will completely delete this playlist and cannot be undone." -msgstr "" -"Questo cancellerà questa lista di riproduzione e non può essere annullato." +msgstr "Questo cancellerà questa lista di riproduzione e non può essere annullato." #: front/src/views/radios/Detail.vue:27 msgid "This will completely delete this radio and cannot be undone." @@ -2180,39 +1710,194 @@ msgstr "Questo cancellerà questa radio e non può essere annullato." #: front/src/components/auth/SubsonicTokenForm.vue:51 msgid "This will completely disable access to the Subsonic API using from account." -msgstr "" -"Questo disabiliterà completamente l'accesso alla API Subsonic dagli account." +msgstr "Questo disabiliterà completamente l'accesso alla API Subsonic dagli account." -#: front/src/App.vue:137 +#: front/src/App.vue:162 src/components/About.vue:55 +#: src/components/Home.vue:154 front/src/components/PageNotFound.vue:33 +#: src/components/Sidebar.vue:203 front/src/components/Sidebar.vue:204 +#: src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 +#: src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 +#: src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 +#: src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 +#: src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 +#: src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 +#: src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 +#: src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 +#: src/components/audio/SearchBar.vue:27 front/src/components/auth/Login.vue:77 +#: src/components/auth/Login.vue:78 front/src/components/auth/Logout.vue:20 +#: src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 +#: src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 +#: src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 +#: src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 +#: src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 +#: src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 +#: src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 +#: src/views/auth/EmailConfirm.vue:51 front/src/views/auth/PasswordReset.vue:52 +#: src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +#: src/views/content/Base.vue:19 front/src/views/content/Home.vue:35 +#: src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 +#: src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 +#: src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 +#: src/views/instance/Timeline.vue:57 front/src/views/playlists/Detail.vue:90 +#: src/views/playlists/List.vue:104 front/src/views/playlists/List.vue:105 +#: src/views/radios/Detail.vue:80 +#, fuzzy msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "" -"Questo cancellerà i tuoi dati locali e ti disconnetterà, vuoi continuare?" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "Questo cancellerà i tuoi dati locali e ti disconnetterà, vuoi continuare?" +msgstr[1] "Questo cancellerà i tuoi dati locali e ti disconnetterà, vuoi continuare?" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." -msgstr "" -"Questo ti disconnetterà dai dispositivi esistenti che utilizzano la password " -"attuale." +msgstr "Questo ti disconnetterà dai dispositivi esistenti che utilizzano la password attuale." #: front/src/components/playlists/Editor.vue:44 msgid "This will remove all tracks from this playlist and cannot be undone." +msgstr "Questo cancellerà tutte le tracce da questa lista di riproduzione e non può essere annullato." + +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." msgstr "" -"Questo cancellerà tutte le tracce da questa lista di riproduzione e non può " -"essere annullato." #: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 #: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 msgid "Title" msgstr "Titolo" -#: front/src/components/audio/SearchBar.vue:27 -#: src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" -msgstr "Traccia" - #: front/src/components/library/Track.vue:53 msgid "Track information" msgstr "Informazioni traccia" @@ -2227,15 +1912,11 @@ msgstr "tracce" #: front/src/components/library/Album.vue:43 #: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 -#: front/src/views/radios/Detail.vue:34 +#: src/views/content/Base.vue:8 front/src/views/content/libraries/Detail.vue:8 +#: src/views/playlists/Detail.vue:50 front/src/views/radios/Detail.vue:34 msgid "Tracks" msgstr "Tracce" -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "Tracce disponibili in questa libreria" - #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" msgstr "Tracce di questo artista" @@ -2248,36 +1929,39 @@ msgstr "Tracce preferite" msgid "tracks listened" msgstr "tracce ascoltate" -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "Inizia scansione" - +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" msgstr "Tipo" +#: front/src/views/content/remote/Card.vue:100 +#: src/views/content/remote/Card.vue:105 +#, fuzzy +msgid "Unfollow" +msgstr "Segui" + +#: front/src/views/content/remote/Card.vue:101 +#, fuzzy +msgid "Unfollow this library?" +msgstr "Portami alla libreria" + #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." -msgstr "" -"Sfortunatamente i proprietari di questa istanza non hanno ancora avuto tempo " -"di completare questa pagina." - -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "Sconosciuto" +msgstr "Sfortunatamente i proprietari di questa istanza non hanno ancora avuto tempo di completare questa pagina." #: front/src/components/Home.vue:37 msgid "Unlimited music" msgstr "Musica illimitata" -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "Non silenziare" - #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" msgstr "Aggiorna avatar" +#: front/src/views/content/libraries/Form.vue:25 +#, fuzzy +msgid "Update library" +msgstr "Gestisci libreria" + #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" msgstr "Aggiorna lista di riproduzione" @@ -2290,8 +1974,10 @@ msgstr "Aggiorna impostazioni" msgid "Update your password" msgstr "Aggiorna la tua password" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" msgstr "Carica" @@ -2299,46 +1985,57 @@ msgstr "Carica" msgid "Upload a new avatar" msgstr "Carica un nuovo avatar" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" -msgstr "File o sorgenti esterne caricati" +#: front/src/views/content/Home.vue:6 +#, fuzzy +msgid "Upload audio content" +msgstr "Carica un nuovo avatar" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/views/content/libraries/FilesTable.vue:54 +#, fuzzy +msgid "Upload date" +msgstr "Carica" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:31 +#, fuzzy +msgid "Upload new tracks" +msgstr "Carica un nuovo avatar" + +#: front/src/views/admin/users/UsersDetail.vue:82 +#, fuzzy +msgid "Upload quota" +msgstr "Carica" + +#: front/src/components/library/FileUpload.vue:99 +#, fuzzy +msgid "Uploaded" +msgstr "Carica" + +#: front/src/components/library/FileUpload.vue:5 +#, fuzzy +msgid "Uploading" +msgstr "Caricamento..." + +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "Caricamento..." -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" msgstr "Usa un'altra istanza" -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "" -"Usa questo riquadro di commento per aggiungere dettagli alla tua richiesta " -"se necessario" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "" -"Usa questa spunta per abilitare/disabilitare la federazione con questa " -"libreria" - #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." -msgstr "" -"Usa questo modulo per richiedere un reset della password. Ti invieremo una " -"email all'indirizzo fornito con le istruzioni per resettare la tua password." - -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "" -"Usa questo modulo per scansionare un'istanza ed impostare la federazione." +msgstr "Usa questo modulo per richiedere un reset della password. Ti invieremo una email all'indirizzo fornito con le istruzioni per resettare la tua password." #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" msgstr "Usati" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" msgstr "Utente" @@ -2346,12 +2043,20 @@ msgstr "Utente" msgid "User activity" msgstr "Attività utente" +#: front/src/components/library/Album.vue:49 +#: src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +#, fuzzy +msgid "User libraries" +msgstr "Librerie" + #: front/src/components/library/Radios.vue:20 msgid "User radios" msgstr "Radio dell'utente" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" msgstr "Nome utente" @@ -2363,21 +2068,32 @@ msgstr "Nome utente o email" msgid "users" msgstr "utenti" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 +#: src/views/admin/users/UsersList.vue:21 front/src/views/admin/Settings.vue:81 msgid "Users" msgstr "Utenti" +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +#, fuzzy +msgid "View files" +msgstr "File della libreria" + #: front/src/components/library/Album.vue:37 #: src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" msgstr "Vedi su MusicBrainz" +#: front/src/views/content/libraries/Form.vue:18 +#, fuzzy +msgid "Visibility" +msgstr "Visibilità lista di riproduzione" + #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" msgstr "Non possiamo aggiungere la traccia alla lista di riproduzione" @@ -2402,12 +2118,21 @@ msgstr "Non possiamo salvare il tuo avatar" msgid "We cannot save your settings" msgstr "Non possiamo salvare le tue impostazioni" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" msgstr "Noi non ti tracciamo o infastidiamo con pubblicità" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:40 +#, fuzzy +msgid "we recommend using Picard for that purpose" msgstr "Ti consigliamo di utilizzare Picard per quello scopo." #: front/src/components/Home.vue:7 @@ -2422,29 +2147,15 @@ msgstr "Ci dispiace, la pagina che hai richiesto non esiste:" msgid "We've received your request, you'll get some groove soon ;)" msgstr "Abbiamo ricevuto la tua richiesta, presto avrai da divertirti ;)" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "Benvenuto" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" msgstr "Benvenuto su Funkwhale" -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "Cosa vuol dire metadata?" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "" -"Quando abilitato, l'importazione automatica importerà autonomamente le nuove " -"tracce pubblicate in questa libreria" - #: front/src/components/Home.vue:24 msgid "Why funkwhale?" msgstr "Perchè Funkwhale?" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" msgstr "Si" @@ -2452,81 +2163,490 @@ msgstr "Si" msgid "Yes, log me out!" msgstr "Si, disconnettimi!" +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" +msgstr "" + #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" msgstr "Sei attualmente connesso come %{ username }" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." -msgstr "Puoi anche saltare questo passaggio ed inserire manualmente i metadati." - -#: front/src/components/Home.vue:136 -msgid "You can invite friends and family to your instance so they can enjoy your music" +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." msgstr "" -"Puoi invitare amici e familiari sulla tua istanza così possono fruire della " -"tua musica" + +#: front/src/components/Home.vue:133 +msgid "You can invite friends and family to your instance so they can enjoy your music" +msgstr "Puoi invitare amici e familiari sulla tua istanza così possono fruire della tua musica" #: front/src/components/library/radios/Builder.vue:7 msgid "You can use this interface to build your own custom radio, which will play tracks according to your criteria." -msgstr "" -"Puoi usare questa interfaccia per creare la tua radio personalizzata, che " -"riprodurrà tracce in accordo con i tuoi criteri." +msgstr "Puoi usare questa interfaccia per creare la tua radio personalizzata, che riprodurrà tracce in accordo con i tuoi criteri." #: front/src/components/auth/SubsonicTokenForm.vue:8 msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." -msgstr "" -"Puoi usarli per godere delle tue liste di riproduzione e musica anche quando " -"non collegato, dal tuo cellulare o tablet, per esempio." +msgstr "Puoi usarli per godere delle tue liste di riproduzione e musica anche quando non collegato, dal tuo cellulare o tablet, per esempio." -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" msgstr "Hai una radio in riproduzione" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" msgstr "Devi selezionare un'istanza per continuare" +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." +msgstr "" + #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" msgstr "Sarai disconnesso da questa sessione e dovrai accedere con una nuova" #: front/src/components/auth/Settings.vue:71 msgid "You will have to update your password on your clients that use this password." -msgstr "" -"Dovrai aggiornare la tua password sui tuoi dispositivi che utilizzano questa " -"password." - -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "Importerai:" +msgstr "Dovrai aggiornare la tua password sui tuoi dispositivi che utilizzano questa password." #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." -msgstr "" -"Il tuo indirizzo email è stato confermato, ora puoi usare il servizio senza " -"limitazioni." +msgstr "Il tuo indirizzo email è stato confermato, ora puoi usare il servizio senza limitazioni." -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" -msgstr "I Tuoi Preferiti" - -#: front/src/components/Home.vue:117 +#: front/src/components/Home.vue:114 msgid "Your music, your way" msgstr "La tua musica, il tuo modo di essere" +#: front/src/views/Notifications.vue:7 +#, fuzzy +msgid "Your notifications" +msgstr "Ultima modifica" + #: front/src/views/auth/PasswordResetConfirm.vue:29 msgid "Your password has been updated successfully." msgstr "La tua password è stata aggiornata con successo." #: front/src/components/auth/Settings.vue:101 msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" -msgstr "" -"La tua password Subsonic sarà cambiata con una nuova e casuale, e sarai " -"disconnesso dai dispositivi che utilizzano ancora la vecchia password " -"Subsonic" +msgstr "La tua password Subsonic sarà cambiata con una nuova e casuale, e sarai disconnesso dai dispositivi che utilizzano ancora la vecchia password Subsonic" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +#, fuzzy +msgid "Activity visibility" +msgstr "Visibilità lista di riproduzione" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 +#: src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "Nessuno tranne me" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 +#: src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "Tutti su questa istanza" + +#: front/src/components/mixins/Translations.vue:18 +#, fuzzy +msgid "Accessed date" +msgstr "Accesso disabilitato" + +#: front/src/components/mixins/Translations.vue:19 +#, fuzzy +msgid "Modification date" +msgstr "Data di scadenza" + +#: front/src/components/mixins/Translations.vue:20 +#, fuzzy +msgid "Imported date" +msgstr "Importa data" + +#: front/src/components/mixins/Translations.vue:22 +#, fuzzy +msgid "Track name" +msgstr "Traccia" + +#: front/src/components/mixins/Translations.vue:23 +#, fuzzy +msgid "Album name" +msgstr "Pagina dell'album" + +#: front/src/components/mixins/Translations.vue:30 +#, fuzzy +msgid "Sign-up date" +msgstr "Registrati" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "Copia tracce dalla tua coda corrente alla playlist" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "Aggiungi a questa playlist" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "La mia eccezionale playlist" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "Tutti" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "Registrati" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "Inserisci il tuo codice di invito (non tiene conto di maiuscole o minuscole)" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "Inserisci il tuo nome utente" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "Inserisci la tua email" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "Password aggiornata" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "Accesso disabilitato" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "Inserisci il tuo nome utente o l'email" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "Accedi" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "Profilo di %{ username }" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "Disconnetti" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "Impostazioni dell'account" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "Rimuovi dai preferiti" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "I Tuoi Preferiti" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "Inserisci un nome di una radio..." + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "Creatore Radio" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "La mia eccezionale radio" + +#: front/src/components/library/radios/Builder.vue:236 +#, fuzzy +msgid "My awesome description" +msgstr "La mia eccezionale radio" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "" + +#: front/src/components/library/FileUpload.vue:240 +#, fuzzy +msgid "A network error occured while uploading this file" +msgstr "C'è stato un errore durante il salvataggio delle tue modifiche" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "Inserisci il nome di un artista..." + +#: front/src/components/library/Track.vue:195 +#: src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "Traccia" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "Pagina Iniziale" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "Mostra/nascondi password" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "The Beatles, Michael Jackson…" + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "The White Album, Thriller…" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "Usa questo riquadro di commento per aggiungere dettagli alla tua richiesta se necessario" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" msgstr[0] "%{ count } traccia è stata aggiunta alla tua coda" msgstr[1] "%{ count } tracce sono state aggiunte alla tua coda" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "Artista, album, traccia..." + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "Cerca per artisti, album, tracce..." + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "Coda mischiata!" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "Traccia precedente" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "Riproduci traccia" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "Metti in pausa" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "Prossima traccia" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "Non silenziare" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "Muto" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "Ripetizione disattivata. Clicca per attivare la ripetizione della singola traccia." + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "Ripeti una singola traccia. Clicca per ripetere l'intera coda." + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "Ripete l'intera coda. Clicca per disattivare la ripetizione." + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "Mischia la tua coda" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "Pulisci la tua coda" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "Richieste di importazione in sospeso" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "Richiesta di seguire in sospeso" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "Inserisci i tuoi criteri di ricerca..." + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "Cerca per titolo, artista, dominio..." + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "Lascia vuoto per un codice casuale" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "Cerca per nome utente, email, codice..." + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "Cerca per nome utente, email, nome..." + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "Federazione" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "" + +#: front/src/views/content/remote/ScanForm.vue:48 +#, fuzzy +msgid "Enter a library url" +msgstr "Inserisci il nome del dominio di una libreria..." + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:226 +#, fuzzy +msgid "Search by title, artist, album..." +msgstr "Cerca per titolo, artista, dominio..." + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:242 +#, fuzzy +msgid "Import went on successfully" +msgstr "Password aggiornata con successo" + +#: front/src/views/content/libraries/FilesTable.vue:259 +#, fuzzy +msgid "Relaunch import" +msgstr "Termina importazione" + +#: front/src/views/content/libraries/Card.vue:58 +#, fuzzy +msgid "Visibility: nobody except me" +msgstr "Nessuno tranne me" + +#: front/src/views/content/libraries/Card.vue:59 +#, fuzzy +msgid "Visibility: everyone on this instance" +msgstr "Tutti su questa istanza" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:61 +#, fuzzy +msgid "Total size of the files in this library" +msgstr "Tracce disponibili in questa libreria" + +#: front/src/views/content/libraries/Form.vue:70 +#, fuzzy +msgid "My awesome library" +msgstr "La mia eccezionale radio" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:74 +#, fuzzy +msgid "Everyone, including other instances" +msgstr "Tutti su questa istanza" + +#: front/src/views/content/libraries/Form.vue:106 +#, fuzzy +msgid "Library updated" +msgstr "Nome della libreria" + +#: front/src/views/content/libraries/Form.vue:109 +#, fuzzy +msgid "Library created" +msgstr "Nome della libreria" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "Radio" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "Timeline dell'Istanza" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "Lista di riproduzione" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "Inserisci il nome di una playlist..." + +#: front/src/views/admin/library/Base.vue:16 +#, fuzzy +msgid "Manage library" +msgstr "In libreria" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "Determina se un account utente è attivo o meno. Gli utenti inattivi non possono accedere o utilizzare il servizio." + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "" + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "Gestisci utenti" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "Impostazioni dell'istanza" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "Informazioni sull'istanza" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "Subsonic" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "Statistiche" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "Segnalazione errore" diff --git a/front/locales/oc/LC_MESSAGES/app.po b/front/locales/oc/LC_MESSAGES/app.po index de2764bfd..7c0ab7a24 100644 --- a/front/locales/oc/LC_MESSAGES/app.po +++ b/front/locales/oc/LC_MESSAGES/app.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: front 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-22 14:12+0200\n" -"PO-Revision-Date: 2018-07-24 17:46+0000\n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" +"PO-Revision-Date: 2018-10-05 21:32+0000\n" "Last-Translator: Quentí \n" "Language-Team: none\n" "Language: oc\n" @@ -30,15 +30,16 @@ msgstr "(%{ index } sus %{ length })" msgid "(empty)" msgstr "(voida)" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "%{ count } sus %{ total } element seleccionat" msgstr[1] "%{ count } sus %{ total } elements seleccionats" -#: front/src/components/Sidebar.vue:116 -#: src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 +#: src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" msgstr[0] "%{ count } pista" @@ -50,7 +51,7 @@ msgid_plural "%{ count } tracks in %{ albumsCount } albums" msgstr[0] "%{ count } pista dins %{ albumsCount } albums" msgstr[1] "%{ count } pistas dins %{ albumsCount } albums" -#: front/src/components/library/radios/Builder.vue:76 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" msgstr[0] "%{ count } pista correspond als filtres seleccionats" @@ -62,6 +63,10 @@ msgid_plural "%{ count } tracks" msgstr[0] "%{ count} pista" msgstr[1] "%{ count} pistas" +#: front/src/views/content/libraries/Quota.vue:11 +msgid "%{ current } used on %{ max } allowed" +msgstr "%{ count } utilizat sus %{ total } autorizat" + #: front/src/components/common/Duration.vue:2 msgid "%{ hours } h %{ minutes } min" msgstr "%{ hours } h %{ minutes } min" @@ -78,10 +83,6 @@ msgstr "%{ user } a aimat una pista" msgid "%{ user } listened to a track" msgstr "%{ user } a escotat una pista" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "Perfil de %{ username }" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" @@ -94,39 +95,29 @@ msgid_plural "%{ count } favorites" msgstr[0] "1 favorit" msgstr[1] "%{ count } favorits" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "1 piste" -msgstr[1] "%{ count } pistas" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "A prepaus de %{ instance }" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "A prepaus de Funkwhale" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "A prepaus d’aquesta instància" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 +#: front/src/views/content/libraries/Detail.vue:48 +msgid "Accept" +msgstr "Acceptar" + +#: front/src/views/content/libraries/Detail.vue:40 msgid "Accepted" msgstr "Acceptat" -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "Accès desactivat" - -#: front/src/components/Home.vue:109 +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" -msgstr "" -"Accedissètz a vòstra musica d’una interfàcia afinada estant, adaptada a çò " -"que compta vertadièrament" +msgstr "Accedissètz a vòstra musica d’una interfàcia afinada estant, adaptada a çò que compta vertadièrament" #: front/src/views/admin/users/UsersDetail.vue:54 msgid "Account active" @@ -136,10 +127,6 @@ msgstr "Compte actiu" msgid "Account settings" msgstr "Paramètres del compte" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "Paramètres del compte" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "Estat del compte" @@ -148,16 +135,18 @@ msgstr "Estat del compte" msgid "Account's email" msgstr "Adreça electronica del compte" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +msgid "Action" +msgstr "Accion" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" msgstr[0] "L’action %{ action } es estada lançada corrèctament sus %{ count } element" msgstr[1] "L’action %{ action } es estada lançada corrèctament sus %{ count } elements" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:60 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "Accions" @@ -165,30 +154,22 @@ msgstr "Accions" msgid "Active" msgstr "Actiu" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "Activitat" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" -msgstr "Actor" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" +msgstr "Ajustar de contengut" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "Ajustar una nòva bibliotèca" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" msgstr "Ajustar de filtres" -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" msgstr "Ajustar de filtre per dire de personalizar vòstra ràdio" -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "Ajustar a la lista de lectura" - #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" @@ -203,10 +184,6 @@ msgstr "Ajustar a la lista de lectura..." msgid "Add to queue" msgstr "Ajustar a la lista" -#: front/src/components/playlists/PlaylistModal.vue:116 -msgid "Add to this playlist" -msgstr "Ajustar a aquesta lista de lectura" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "Ajustar una pista" @@ -215,25 +192,19 @@ msgstr "Ajustar una pista" msgid "Admin" msgstr "Admin" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" msgstr "Administracion" -#: front/src/components/audio/SearchBar.vue:26 -#: src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 +#: src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "Album" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "Album %{ title } (%{ count } pista) per %{ artist }" -msgstr[1] "Album %{ title } (%{ count } pistas) per %{ artist }" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" @@ -246,7 +217,6 @@ msgstr "Pagina de l’album" #: front/src/components/audio/Search.vue:19 #: src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "Albums" @@ -255,8 +225,8 @@ msgstr "Albums" msgid "Albums by this artist" msgstr "Albums d’aqueste artista" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "Tot" @@ -266,49 +236,23 @@ msgstr "Una error s’es producha en enregistrar vòstras modificacions" #: front/src/components/auth/Login.vue:10 msgid "An unknown error happend, this can mean the server is down or cannot be reached" -msgstr "" -"Una error desconeguda encontrada, aquò pòt significar que lo servidor es " -"fòra servici o pòt pas èsser atengut" - -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "Totes" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "API" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "Aprovar" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "Aprovar l’accès ?" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "Aprovat" +msgstr "Una error desconeguda encontrada, aquò pòt significar que lo servidor es fòra servici o pòt pas èsser atengut" #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "Volètz vertadièrament vos desconnectar ?" -#: front/src/components/audio/SearchBar.vue:25 -#: src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 +#: src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" msgstr "Artista" #: front/src/components/requests/Form.vue:5 +#: src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "Nom de l’artista" @@ -317,26 +261,20 @@ msgstr "Nom de l’artista" msgid "Artist page" msgstr "Pagina de l’artista" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "Artista, album, pista..." - #: front/src/components/audio/Search.vue:10 #: src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 -#: src/components/library/Library.vue:7 +#: front/src/components/library/Library.vue:7 +#: src/components/library/Artists.vue:120 msgid "Artists" msgstr "Artistas" #: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 -#: src/components/library/Radios.vue:44 +#: src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 -#: src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" msgstr "Ascendent" @@ -344,10 +282,6 @@ msgstr "Ascendent" msgid "Ask for a password reset" msgstr "Demandar un nòu senhal" -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "Importacion automatica" - #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" msgstr "Listas de lectura disponiblas" @@ -361,14 +295,9 @@ msgstr "Avatar" msgid "Back to login" msgstr "Tornar a la pagina de connexion" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "" -"Atencion en acceptar de demandas d’abonament, vòl dire que los seguidors " -"poiràn accedir a vòstra bibliotèca complèta." - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "Debit" @@ -376,7 +305,7 @@ msgstr "Debit" msgid "Browse" msgstr "Percórrer" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "Percórrer la bibliotèca" @@ -384,18 +313,6 @@ msgstr "Percórrer la bibliotèca" msgid "Browsing artists" msgstr "Percórrer los artistas" -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "Percórrer las pistas federadas" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "Percórrer los seguidors" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "Percórrer las bibliotècas" - #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" msgstr "Percórrer las listas de lectura" @@ -412,22 +329,17 @@ msgstr "Editor" msgid "By %{ artist }" msgstr "De %{ artist }" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." -msgstr "Se confirmatz, %{ username } poirà pas accedir a vòstra bibliotèca." +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." +msgstr "En quitar de seguir aquesta bibliotèca, perdretz l’accès a son contengut." -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "Se confirmatz, %{ username } poirà accedir a vòstra bibliotèca." - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" msgstr "Anullar" -#: front/src/components/library/radios/Builder.vue:59 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "Pistas candidatas" @@ -435,7 +347,7 @@ msgstr "Pistas candidatas" msgid "Cannot change your password" msgstr "Lo senhal pòt pas se cambiar" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" msgstr "Cambiar la lenga" @@ -462,15 +374,13 @@ msgstr "Cambiament sincronizat amb lo servidor" #: front/src/components/auth/Settings.vue:70 msgid "Changing your password will also change your Subsonic API password if you have requested one." -msgstr "" -"L’actualizacion de vòstra senhal cambiarà tanben lo de l’API Subsonic se n’" -"avètz un." +msgstr "L’actualizacion de vòstra senhal cambiarà tanben lo de l’API Subsonic se n’avètz un." #: front/src/components/auth/Settings.vue:98 msgid "Changing your password will have the following consequences" msgstr "Lo cambiament de senhal a las consequéncias seguentas" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "Causissètz vòstra instància" @@ -487,22 +397,13 @@ msgstr "Escafar" msgid "Clear playlist" msgstr "Escafar la lista de lectura" -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "Voidar la fila" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "CLI" - #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" msgstr "Amb un clic, escotatz d’oras de musica a la ràdio" -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" -msgstr "Tampada" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" +msgstr "Clicatz per causir los fichièrs d’enviar o lisatz los fichièrs o repertòris" #: front/src/components/manage/users/InvitationForm.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:42 @@ -515,12 +416,11 @@ msgstr "Còdi" msgid "Collapse" msgstr "Plegar" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "Comentari" -#: front/src/components/library/radios/Builder.vue:58 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" msgstr "Configuracion" @@ -537,11 +437,11 @@ msgstr "Confirmar vòstra adreça electronica" msgid "Confirmation code" msgstr "Còdi de confirmacion" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" -msgstr "Copiar las pistas de la fila a la lista de lectura" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" +msgstr "Copiar" -#: front/src/components/Home.vue:88 +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" msgstr "Jaqueta d’albums, paraulas, nòstra tòca es d’o aver tot ;)" @@ -549,19 +449,26 @@ msgstr "Jaqueta d’albums, paraulas, nòstra tòca es d’o aver tot ;)" msgid "Create a funkwhale account" msgstr "Crear un compte funkwhale" +#: front/src/views/content/libraries/Home.vue:14 +msgid "Create a new library" +msgstr "Crear una nòva bibliotèca" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "Crear una nòva lista de lectura" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "Crear un compte" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "Crear un import" +#: front/src/views/content/libraries/Form.vue:26 +msgid "Create library" +msgstr "Crear una bibliotèca" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "Crear mon compte" @@ -574,9 +481,8 @@ msgstr "Crear una lista de lectura" msgid "Create your own radio" msgstr "Crear vòstra pròpria ràdio" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "Data de creacion" @@ -584,17 +490,34 @@ msgstr "Data de creacion" msgid "Current avatar" msgstr "Avatar actual" +#: front/src/views/content/libraries/DetailArea.vue:4 +msgid "Current library" +msgstr "Bibliotèca actuala" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "Pista actuala" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +msgid "Current usage" +msgstr "Utilizacion actuala" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "Data" + +#: front/src/views/content/libraries/Form.vue:29 +#: src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "Suprimir" +#: front/src/views/content/libraries/Form.vue:39 +msgid "Delete library" +msgstr "Suprimir la bibliotèca" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "Suprimir la lista de lectura" @@ -603,36 +526,32 @@ msgstr "Suprimir la lista de lectura" msgid "Delete radio" msgstr "Suprimir la ràdio" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" -msgstr "Refusar" - -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "Refusar l’accès ?" +#: front/src/views/content/libraries/Form.vue:31 +msgid "Delete this library?" +msgstr "Suprimir aquesta bibliotèca ?" #: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 -#: src/components/library/Radios.vue:47 +#: src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 -#: src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "Descendent" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +msgid "Description" +msgstr "Descripcion" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "Detalhs" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." -msgstr "" -"Determina se l’utilizaire es actiu o non. Los compte inactius se pòdon pas " -"connectar nimai utilizar lo servici." +#: front/src/views/content/remote/Card.vue:50 +msgid "Details" +msgstr "Detalhs" #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 @@ -651,7 +570,7 @@ msgstr "Desactivar l’accès a l’API Subsonic ?" msgid "Discover how to use Funkwhale from other apps" msgstr "Aprenètz a utilizar Funkwhale amb d’autras aplicacions" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "Mostrar publicament" @@ -677,41 +596,43 @@ msgid_plural "Do you want to launch %{ action } on %{ count } elements?" msgstr[0] "Volètz lançar l’accion « %{ action } » sus %{ count } element ?" msgstr[1] "Volètz lançar l’accion « %{ action } » sus %{ count } elements ?" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "Volètz restablir vòstra fila precedenta ?" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "Documentacion" -#: front/src/components/audio/track/Table.vue:24 -#: src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "Telecargar" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "Telecargar las pistas" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" msgstr "Lisatz las linhas per triar las pistas de la lista de lectura" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 +#: src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "Durada" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "Simple d’utilizar" +#: front/src/views/content/libraries/Detail.vue:9 +msgid "Edit" +msgstr "Modificar" + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "Editrar las informacions d’aquesta instància" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "Editar..." @@ -732,57 +653,16 @@ msgstr "Corrièl confirmat" msgid "End edition" msgstr "Acabar l’edicion" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "" -"Verificatz que vòstres fichièrs musicals son corrèctament etiquetats abans " -"de los enviar." - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "Escrivètz un nom de ràdio..." - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "Escrivètz un nom d’artista..." - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "Escrivètz un nom de domeni..." - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "Escrivètz un nom de lista de lectura..." - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "Escrivètz vòstre adreça electronica" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "Escrivètz vòstre còdi d’invitacion (pas sensible a la cassa)" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "Escrivètz vòstra recèrca..." - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "Escrivètz vòstre nom d’utilizaire" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "Escrivètz vòstre nom d’utilizaire o corrièl" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "Error" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" -msgstr "Rapòrt d’error" +#: front/src/views/content/remote/Card.vue:39 +msgid "Error during scan" +msgstr "Error en explorant" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "Error en tractar l’accion" @@ -802,29 +682,28 @@ msgstr "Error en confirmar l’adreça electronica" msgid "Error while creating invitation" msgstr "Error en crear l’invitacion" +#: front/src/views/content/remote/ScanForm.vue:3 +msgid "Error while fetching remote library" +msgstr "Error en recuperar la bibliotèca alonhada" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "Error en enregistrar los paramètres" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "Error en analizar la bibliotèca" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "Perturbat" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" -msgstr "Tot lo monde" +#: front/src/views/content/libraries/Quota.vue:75 +msgid "Errored files" +msgstr "Fichièrs amb errors" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" -msgstr "Lo monde d’aquesta instància" +#: front/src/views/content/remote/Card.vue:58 +msgid "Errored tracks:" +msgstr "Pistas en error :" -#: front/src/components/library/radios/Builder.vue:57 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "Exclure" @@ -833,6 +712,7 @@ msgid "Expand" msgstr "Dobrir" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "Data d’expiracion" @@ -844,97 +724,50 @@ msgstr "Expirada" msgid "Expired/used" msgstr "Expirada/utilizada" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "Font extèrna. Servicis compatibles" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "Favorits" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "Federar amb una nòva instància" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "Pistas federadas" - -#: front/src/components/Sidebar.vue:87 -#: src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 -#: src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 -#: src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "Federacion" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "Còpia dels fichièrs" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "Nom del fichièr" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "Mandadís del fichièr" - #: front/src/views/admin/library/Base.vue:5 #: src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "Fichièrs" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "Filtrar lo tipe d’album" - -#: front/src/components/library/radios/Builder.vue:56 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "Nom del filtre" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "Acabar l’import" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "Acabat" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "D’en primièr, causissètz lo mòde d’import per la musica" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "Seguir" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" +#: front/src/views/content/remote/Card.vue:88 +msgid "Follow pending approval" msgstr "Abonament en espèra de validacion" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" -msgstr "Estatut de l’abonament" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" +msgstr "Seguir de bibliotècas alonhadas" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "Seguidors" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "Seguidors solament" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "Abonat" -#: front/src/components/activity/Like.vue:12 -#: src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 +#: src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "de l’album %{ album } per %{ artist }" @@ -942,30 +775,23 @@ msgstr "de l’album %{ album } per %{ artist }" msgid "From album %{ album } by %{ artist }" msgstr "De l’album %{ album } per %{ artist }" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" -msgstr "" -"Funkwhale es un projècte liure e gratuit, menat per de benevols. Podètz " -"ajudar a melhorar lo servici en senhalar los problèmas que trobatz, en " -"suggerir de melhoraments e ne’n parlant altorn de vos !" +msgstr "Funkwhale es un projècte liure e gratuit, menat per de benevols. Podètz ajudar a melhorar lo servici en senhalar los problèmas que trobatz, en suggerir de melhoraments e ne’n parlant altorn de vos !" #: front/src/components/auth/SubsonicTokenForm.vue:7 msgid "Funkwhale is compatible with other music players that support the Subsonic API." -msgstr "" -"Funkwhale es compatible amb d’autres lectors de musica compatibles amb l’API " -"Subsonic." +msgstr "Funkwhale es compatible amb d’autres lectors de musica compatibles amb l’API Subsonic." -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "Funkwhale es simple d’utilizar." #: front/src/components/Home.vue:39 msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." -msgstr "" -"Funkwhale es concebut per facilitar l’escota de las musicas que vos agradan " -"e descobrir de novèls artistas." +msgstr "Funkwhale es concebut per facilitar l’escota de las musicas que vos agradan e descobrir de novèls artistas." -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." msgstr "Funkwhale es a gratís e vos dòna lo contròla de vòstra musica." @@ -981,17 +807,15 @@ msgstr "Obténer una novèla invitacion" msgid "Get me to the library" msgstr "Menatz-me a la bibliotèca" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " +#: front/src/components/Home.vue:76 +msgid "Get quality metadata about your music thanks to MusicBrainz" msgstr "" -"Obtenètz de metadonadas de qualitat per vòstra musica gràcia a\n" -" \n" -" MusicBrainz\n" -" " +"Obtenètz de metadonadas de qualitat per vòstra musica gràcia a MusicBrainz" + +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +msgid "Get started" +msgstr "Començar" #: front/src/components/common/ActionTable.vue:21 #: front/src/components/common/ActionTable.vue:27 @@ -1002,175 +826,57 @@ msgstr "Zo" msgid "Go to home page" msgstr "Tornar a l’acuèlh" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "Recuperar las metadonadas ligadas" - -#: front/src/App.vue:74 +#: front/src/App.vue:73 msgid "Help us translate Funkwhale" msgstr "Ajudar a traduire Funkwhale" -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "Acuèlh" - #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" msgstr "Oras de musica" #: front/src/components/auth/SubsonicTokenForm.vue:11 msgid "However, accessing Funkwhale from those clients require a separate password you can set below." -msgstr "" -"Pr’aquò, accedir a Funkwhale d’un client estant demanda un senhal diferent " -"que podètz configurar çai-jos." - -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "ID" +msgstr "Pr’aquò, accedir a Funkwhale d’un client estant demanda un senhal diferent que podètz configurar çai-jos." #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." -msgstr "" -"Se l’adreça qu’avètz provesida es valida e associada a un compte utilizaire, " -"sètz per recebre un messatge amb las consignas de reïnicializacion d’aquí " -"una estona." - -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "Import" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "Import #%{ id } aviat" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "Import %{ count } pista" -msgstr[1] "Import %{ count } pistas" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "Part d’import" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "Part d’imports #%{ id }" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "Parts d’imports" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "Parts d’imports" +msgstr "Se l’adreça qu’avètz provesida es valida e associada a un compte utilizaire, sètz per recebre un messatge amb las consignas de reïnicializacion d’aquí una estona." #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "Data d’import" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "Pagina de detalhs de l’import" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "Importar de musica" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "Importar de musica" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" msgstr "Importatz la musica de diferentas plataforma, coma YouTube o Soundcloud" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" -msgstr "Import en espèra" +#: front/src/components/library/FileUpload.vue:51 +msgid "Import reference" +msgstr "Importar la referéncia" -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "Requèstas d’import" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" -msgstr "Font de l’import" - -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "Estatut de l’import" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "Importar aquesta album" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "Importar aquesta pista" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "Importat" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "URL importadas" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "Imports" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "Als favorits" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "A la bibliotèca" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "Actiu" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "Picar manualament un ID MasicBrainz :" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "Escrivètz l’adreça ligada a vòstre compte" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" msgstr[0] "Inserir de la fila (%{ count } pista)" msgstr[1] "Inserir de la fila (%{ count } pistas)" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "Info. de l’instància" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "Ràdios de l’instància" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "Paramètres de l’intància" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "Flux de l’instància" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1186,51 +892,41 @@ msgstr "Còdi d’invitacion (opcional)" msgid "Invitations" msgstr "Invitacions" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "Traçador de problèmas" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "ID de la tasca" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" -msgstr "Tascas" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" +msgstr "Sembla qu’avètz pas cap de bibliotèca pel moment, es ora de ne crear una !" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" msgstr "Gardatz una traça de vòstras cançons favoritas" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." -msgstr "" -"Gardatz vòstre PRIVATE_TOKEN secrèt estant que permet l’accès a vòstre " -"compte." +#: front/src/views/content/remote/Home.vue:14 +msgid "Known libraries" +msgstr "Bibliotècas conegudas" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "Darrièra activitat" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "Darrièra recuperacion" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "Darrièra modificacion" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +msgid "Last update:" +msgstr "Darrièra actualizacion :" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "Aviar" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "Data de lançament" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" msgstr "Aviar l’analisi" @@ -1238,25 +934,21 @@ msgstr "Aviar l’analisi" msgid "Learn more about this instance" msgstr "Ne saber mai tocant aquesta instància" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "Daissar void per obténer un còdi aleatòri" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "Daissar aqueste camp void se volètz suggerir totas las discografia." -#: front/src/views/federation/Base.vue:5 -#: src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "Bibliotècas" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "Las bibliotècas vos ajudan a organizar e partejar vòstras colleccions de musica. Podètz enviar vòstra pròpria collecion musicala a Funkwhale e la partejar amb vòstres amics e vòstra familha." + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "Bibliotèca" @@ -1264,161 +956,111 @@ msgstr "Bibliotèca" msgid "Library files" msgstr "Fichièrs de la bibliotèca" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "Nom de la bibliotèca" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "Talha de la bibliotèca" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "library@demo.funkwhale.audio" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "Ligams" +#: front/src/views/content/libraries/Detail.vue:21 +msgid "Loading followers..." +msgstr "Cargament dels seguidors..." + +#: front/src/views/content/libraries/Home.vue:3 +msgid "Loading Libraries..." +msgstr "Cargament de las bibliotècas..." + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +msgid "Loading library data..." +msgstr "Cargament de las donadas de la bibliotèca..." + +#: front/src/views/Notifications.vue:4 +msgid "Loading notifications..." +msgstr "Cargament de las notificacions..." + +#: front/src/views/content/remote/Home.vue:3 +msgid "Loading remote libraries..." +msgstr "Cargament de las bibliotècas alonhadas..." + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "Cargament del flux d’actualitat..." +#: front/src/views/content/libraries/Quota.vue:4 +msgid "Loading usage data..." +msgstr "Cargament de l’utilizacion de las donadas..." + #: front/src/components/favorites/List.vue:5 msgid "Loading your favorites..." msgstr "Cargament de vòstres favorits..." -#: front/src/components/auth/Login.vue:78 -msgid "Log In" -msgstr "Connexion" - #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "Connectatz-vos a vòstre compte Funkwhale" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "Desconnexion" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "Connectat coma %{ username }" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "Connexion" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "Desconnexion" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "" -"Repeticion desactivada. Clicatz per activar la repeticion de la pista " -"actuala." - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "" -"Repeticion de la pista actuala. Clicatz per activar la repeticion de tota la " -"fila." - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "Repeticion de tota la fila, clicatz per desactivar la repeticion." - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "Paraulas" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "Gerir la bibliotèca" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "Gerir las listas de lectura" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "Gerir los utilizaires" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "Gerir vòstras listas de lectura" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" -msgstr "Marcar coma tampat" +#: front/src/views/Notifications.vue:17 +msgid "Mark all as read" +msgstr "Las marcar totas coma legidas" -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" -msgstr "Marcar coma importat" +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" +msgstr "Mo" -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "Metadonadas" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." -msgstr "" -"Las metadonadas son d’informacions relativas a la musica que volètz " -"importar. Aquò concerna totas las informacions tocant los artistas, album e " -"pistas. Per dire d’aver de metadonadas de qualitat, es recomandat d’emplegar " -"las metadonadas del projècte\n" -" \n" -" MusicBrainz\n" -" \n" -" , que podètz veire coma lo Wikipèdia de la musica." - -#: front/src/components/Sidebar.vue:48 -#: src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "Musica" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "Requèsta musicala" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "Copar lo son" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "Mon compte" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" -msgstr "Ma lista de lectura tròp crana" +#: front/src/views/content/libraries/Home.vue:6 +msgid "My libraries" +msgstr "Mas bibliotècas" -#: front/src/components/library/radios/Builder.vue:227 -msgid "My awesome radio" -msgstr "Ma ràdio tròp crana" - -#: front/src/components/library/Track.vue:64 -#: src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 +#: src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 +#: src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "ND" #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "Nom" @@ -1427,48 +1069,39 @@ msgstr "Nom" msgid "New password" msgstr "Nòu senhal" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "Las novèlas pistas seràn automaticament ajustadas aquí." -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "Etapa seguenta" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "Pista seguenta" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "Non" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" msgstr "Cap d’extension d’installar, vos cal pas qu’una bibliotèca sul web" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "Cap de paraulas pas disponiblas per aquesta pista." -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" -msgstr "Degun fòra ieu" +#: front/src/components/federation/LibraryWidget.vue:6 +msgid "No matching library." +msgstr "Cap de bibliotèca correspondenta." -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "Pas abonat" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" -msgstr "Pas importat" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" +msgstr "Degun sèc pas aquesta bibliotèca" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "Pas utilizat" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +msgid "Notifications" +msgstr "Notificacions" + +#: front/src/App.vue:36 msgid "Official website" msgstr "Site oficial" @@ -1476,47 +1109,32 @@ msgstr "Site oficial" msgid "Old password" msgstr "Senhal precedent" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "" -"Un còp totes los fichièrs cargats, clicatz lo boton seguent per verificar l’" -"estatut de l’import." - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 msgid "Open" msgstr "Accès liure" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "Opcions" -#: front/src/components/library/import/Main.vue:93 -msgid "Or" -msgstr "O" - #: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 -#: src/components/library/Radios.vue:33 +#: src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 -#: src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" msgstr "Òrdre" #: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 -#: src/components/library/Radios.vue:41 +#: src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 -#: src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "Direccion" @@ -1524,10 +1142,6 @@ msgstr "Direccion" msgid "Owner" msgstr "Proprietari" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "Pagina pas trobada" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "Pagina pas trobada !" @@ -1536,42 +1150,25 @@ msgstr "Pagina pas trobada !" msgid "Password" msgstr "Senhal" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "Senhal actualizat" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "Senhal corrèctament modificat" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "Pausar" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "En espèra" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" msgstr "En espèra de validacion" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" -msgstr "Demandas d’abonament en espèra" +#: front/src/views/content/libraries/Quota.vue:22 +msgid "Pending files" +msgstr "Fichièrs en espèra" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "Requèstas en espèra" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "Requèsta en espèra" @@ -1595,10 +1192,6 @@ msgstr "O legir tot" msgid "Play all albums" msgstr "Legir totes los albums" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "Legir sulcòp" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "Legir en seguida" @@ -1607,14 +1200,6 @@ msgstr "Legir en seguida" msgid "Play now" msgstr "Legir ara" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "Legir" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "Lista de lectura" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1641,9 +1226,9 @@ msgstr "Lista de lectura actualizada" msgid "Playlist visibility" msgstr "Visibilitat de la lista de lectura" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" msgstr "Listas de lectura" @@ -1657,50 +1242,50 @@ msgstr "Mercés de verificar que lo senhal es corrèct" #: front/src/components/auth/Login.vue:9 msgid "Please double-check your username/password couple is correct" -msgstr "Mercés de verificar que lo nom d’utilizaire e lo senhal son corrècts" +msgstr "Mercés de verificar que lo nom d’utilizaire e lo senhal son corrèctes" #: front/src/components/auth/Settings.vue:46 msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." -msgstr "" -"PNG, GIF o JPG. 2Mo al maximum. L’imatge serà retalhat en 400×400 pixèls." +msgstr "PNG, GIF o JPG. 2Mo al maximum. L’imatge serà retalhat en 400×400 pixèls." -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "Etapa precedenta" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" -msgstr "Pista precedenta" +#: front/src/components/library/FileUpload.vue:58 +msgid "Proceed" +msgstr "Contunhar" #: front/src/views/auth/EmailConfirm.vue:26 #: front/src/views/auth/PasswordResetConfirm.vue:31 msgid "Proceed to login" msgstr "Contunhar cap a la pagina de connexion" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" -msgstr "Data de sortida" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" +msgstr "Tractament" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" -msgstr "Modèl de recèrca" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "Purgar" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "Purgar los fichièrs amb errors ?" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "Purgar los fichièrs en espèra ?" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" +msgstr "Purgar los fichièrs ignorats ?" #: front/src/components/Sidebar.vue:20 msgid "Queue" msgstr "Fila" -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "La fila es estada mesclada !" - -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "Ràdio" - -#: front/src/components/library/radios/Builder.vue:226 -msgid "Radio Builder" -msgstr "Editor de ràdio" - #: front/src/components/library/radios/Builder.vue:15 msgid "Radio created" msgstr "Nom de la ràdio" @@ -1714,7 +1299,7 @@ msgid "Radio updated" msgstr "Ràdio actualizada" #: front/src/components/library/Library.vue:10 -#: src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 msgid "Radios" msgstr "Ràdios" @@ -1734,33 +1319,39 @@ msgstr "Ajustadas als favorits i a res" msgid "Recently listened" msgstr "Escotadas i a res" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "ID MuzicBrainz de l’enregistrament" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" msgstr "Actualizar" -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "Refusat" - #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" msgstr "Marcat dempuèi %{ date }" #: front/src/components/auth/Signup.vue:9 msgid "Registration are closed on this instance, you will need an invitation code to signup." -msgstr "" -"Las inscripcions son tampadas sus aquestà instància, aurètz de téner un còdi " -"d’invitacion per vos marcar." +msgstr "Las inscripcions son tampadas sus aquestà instància, aurètz de téner un còdi d’invitacion per vos marcar." #: front/src/components/manage/users/UsersTable.vue:71 msgid "regular user" msgstr "utilizaire estandard" +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "Regetar" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "Regetat" + +#: front/src/views/content/remote/Home.vue:6 +msgid "Remote libraries" +msgstr "Bibliotècas alonhadas" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." +msgstr "Las bibliotècas alonhadas apertenon a d’autres utilizaires del malhum. I podètz accedir tant que sián publicas o qu’ajatz l’autorizacion." + #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" msgstr "Tirar" @@ -1769,10 +1360,6 @@ msgstr "Tirar" msgid "Remove avatar" msgstr "Suprimir l’avatar" -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "Tirar dels favorits" - #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" @@ -1786,109 +1373,60 @@ msgstr "Demandar un nòu senhal per l’API Subsonic ?" msgid "Request a password" msgstr "Demandar un senhal" -#: front/src/App.vue:35 -msgid "Request music" -msgstr "Demandas de musica" - -#: front/src/views/library/MusicRequest.vue:4 -#: src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "Demandatz de musica" - #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" msgstr "Requèsta enviada" -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "Tornar aviar las tascas fracassadas" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "Reaviar la tasca" - #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" msgstr "Reïnicializar lo senhal" -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "Resultat %{ current }/%{ total }" - #: front/src/components/favorites/List.vue:38 #: src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 -#: src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" msgstr "Resultats per pagina" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:29 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" msgstr "Enregistrar" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" -msgstr "Analisi aviat !" +#: front/src/views/content/remote/Card.vue:31 +msgid "Scan pending" +msgstr "Exploracion en espèra" + +#: front/src/views/content/remote/Card.vue:43 +msgid "Scanned successfully" +msgstr "Exploracion corrèctament realizada" + +#: front/src/views/content/remote/Card.vue:47 +msgid "Scanned with errors" +msgstr "Explorada amb d’errors" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" +msgstr "Exploracion... (%{ progress }%)" -#: front/src/components/federation/LibraryTrackTable.vue:5 #: front/src/components/library/Artists.vue:10 #: src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:5 #: src/views/playlists/List.vue:13 msgid "Search" msgstr "Recercar" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "Recercar una ressorga que volètz importar :" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "Recercar per artista, nom d’utilizaire, comentari..." - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "Recercar per font..." - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "Recercar per utilizaire, font..." - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "Recercar per títol, artista, domeni..." - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "Recercar per nom d’utilizaire, domeni..." - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "Recercar per nom d’utilizaire, corrièl, còdi..." - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "Recercar per nom d’utilizaire, corrièl, nom..." - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." -msgstr "Recercar d’artistas, d’albums, de pistas..." +#: front/src/views/content/remote/ScanForm.vue:9 +msgid "Search a remote library" +msgstr "Cercar una bibliotèca alonhada" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" msgstr "Recercar de musica" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" msgstr "Recercar sus lyrics.wikia.com" @@ -1898,43 +1436,27 @@ msgstr "Recercar sus lyrics.wikia.com" msgid "Search on Wikipedia" msgstr "Recercar sus Wikipèdia" -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "Recèrca" - #: front/src/views/admin/Settings.vue:15 msgid "Sections" msgstr "Seccions" -#: front/src/components/library/radios/Builder.vue:41 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" msgstr "Seleccionar un filtre" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" msgstr[0] "Seleccionar l’ensemble dels %{ total } element" msgstr[1] "Seleccionar l’ensemble dels %{ total } elements" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "Seleccionar los fichièrs per enviar..." - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" msgstr "Seleccionar solament la pagina actuala" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "Seleccionar las fonts o los fichièrs d’importar" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "Enviar una demanda d’abonament" - -#: front/src/components/Sidebar.vue:97 -#: src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" msgstr "Paramètres" @@ -1946,69 +1468,64 @@ msgstr "Paramètres actualizats" msgid "Settings updated successfully." msgstr "Paramètres corrèctament modificats." -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "Paramètres..." - #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" msgstr "Ligam de partatge" +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." +msgstr "Partajatz aqueste ligam amb d’autres utilizaires per que pòscan accedir a vòstra bibliotèca." + +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +msgid "Sharing link" +msgstr "Ligam de partatge" + +#: front/src/components/audio/album/Card.vue:40 +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "%{ count } pista mai" +msgstr[1] "%{ count } pistas mai" + #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" msgstr[0] "Mostrar 1 album mai" msgstr[1] "Mostrar %{ count } albums mai" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "Mostrar 1 pista mai" -msgstr[1] "Mostrar %{ count } pistas mai" +#: front/src/views/Notifications.vue:10 +msgid "Show read notifications" +msgstr "Mostrar las notificacions legidas" -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" -msgstr "Mostrar/amagar lo senhal" - -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" msgstr "Afichatge dels resultats %{ start }-%{ end } sus %{ total }" -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "Mesclar la fila" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "Inscripcion" - #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" msgstr "Inscripcion" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "Copiatz lo tèxte çai-jos dins un terminal per lançar lo telecargament." - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 +#: src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" msgstr "Talha" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 msgid "Skipped" msgstr "Ignorat" +#: front/src/views/content/libraries/Quota.vue:49 +msgid "Skipped files" +msgstr "Fichièrs ignorats" + #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "Manca quicòm a la bibliotèca ? Digatz-nos çò que volètz escotar !" @@ -2019,24 +1536,17 @@ msgstr "O planhèm, avèm pas trobat cap d’album que correspond a vòstra rec #: front/src/components/audio/Search.vue:16 msgid "Sorry, we did not found any artist matching your query" -msgstr "" -"O planhèm, avèm pas trobat cap d’artista que correspond a vòstra recèrca" +msgstr "O planhèm, avèm pas trobat cap d’artista que correspond a vòstra recèrca" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "Font" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "Còdi font" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "Còdi font (%{version})" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" msgstr "Membre de la còla" @@ -2045,26 +1555,11 @@ msgstr "Membre de la còla" msgid "Start" msgstr "Aviar" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "Aviar lo mandadís" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "Estatisticas" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "Estatut" @@ -2072,15 +1567,11 @@ msgstr "Estatut" msgid "Stop" msgstr "Arrestar" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" msgstr "Arrestar la ràdio" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "Arrestar lo mandadís" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" msgstr "Validar" @@ -2088,95 +1579,85 @@ msgstr "Validar" msgid "Submit another request" msgstr "Enviar una novèla requèsta" -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "Prepausat per" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "Subsonic" - #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" msgstr "Senhal de l’API Subsonic" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" -msgstr "Succès" - -#: front/src/App.vue:11 +#: front/src/App.vue:13 msgid "Suggested choices" msgstr "Suggestions" +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" +msgstr "Resumit" + #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "Sincronizacion dels cambiaments amb lo servidor..." +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "Tèxte copiat al quichapapièr !" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." -msgstr "" -"Es simple : nos agradava Grooveshark e voliam construire quicòm de melhor." +msgstr "Es simple : nos agradava Grooveshark e voliam construire quicòm de melhor." -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "Los Beatles, Mickael Jackson…" - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." msgstr "Lo logotipe de Funkwhale foguèt dessenhat e fornit per Francis Gading." -#: front/src/components/Home.vue:124 +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." +msgstr "Aquesta bibliotèca e totas sas pistas seràn suprimidas. Aquesta accion se pòt pas anullar." + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "Los fichièrs musicals que sètz a enviar son etiquetats coma cal :" + +#: front/src/components/Home.vue:121 msgid "The plaform is free and open-source, you can install it and modify it without worries" -msgstr "" -"La plataforma es gratuita e liura, podètz l’installar e la modificar sens " -"cap de limit" +msgstr "La plataforma es gratuita e liura, podètz l’installar e la modificar sens cap de limit" #: front/src/components/auth/SubsonicTokenForm.vue:4 msgid "The Subsonic API is not available on this Funkwhale instance." msgstr "L’API Subsonic es pas disponibla per aquesta instància Funkwhale." -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" -msgstr "L'Album Blanc, Thriller…" +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" +msgstr "Los fichièrs enviats son al format OGG, Flac o MP3" -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." -msgstr "" -"Es pas possible pel moment de telecargar un ensemble de pistas coma un " -"archiu ZIP. Pr’aquò, podètz utilizar una aisina en linha de comanda coma " -"cURL per telecargar facilament una lista de pistas." +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" +msgstr "Aqueste album es present a las bibliotècas seguentas :" -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." -msgstr "" -"Aqueste import serà associat a la requèsta çai-jos. Un còp acabat, la " -"requèsta sera marcada coma completada." +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" +msgstr "Aqueste artista es present a las bibliotècas seguentas :" -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" -msgstr "Aquò indica se la bibliotèca alonhada vos a donat l’accès" +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." +msgstr "Aquesta instància provesís fins a %{quota} d’espaci per cada utilizaire." #: front/src/components/auth/Profile.vue:16 msgid "This is you!" msgstr "Sètz vos !" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." -msgstr "" -"Aquesta operacion pòt afectar mantun elements, mercés de verificar s’es ben " -"çò que desiratz." +msgstr "Aquesta operacion pòt afectar mantun elements, mercés de verificar s’es ben çò que desiratz." -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" -msgstr "Aquesta pista es pas importada e òm pòt pas la legir" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "Utilizarem aquesta referéncia per gropar los fichièrs importats amassa." + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" +msgstr "Aquesta pista es presenta a las bibliotècas seguentas :" #: front/src/views/playlists/Detail.vue:37 msgid "This will completely delete this playlist and cannot be undone." -msgstr "" -"Aquò escafarà aquesta lista de lectura per totjorn e poirà pas èsser anullat." +msgstr "Aquò escafarà aquesta lista de lectura per totjorn e poirà pas èsser anullat." #: front/src/views/radios/Detail.vue:27 msgid "This will completely delete this radio and cannot be undone." @@ -2184,39 +1665,193 @@ msgstr "Aquò escafarà aquesta ràdio per totjorn e poirà pas èsser anullat." #: front/src/components/auth/SubsonicTokenForm.vue:51 msgid "This will completely disable access to the Subsonic API using from account." -msgstr "" -"Aquò desactivarà complètament l’accès a l’API Subsonic de vòstre compte " -"estant." +msgstr "Aquò desactivarà complètament l’accès a l’API Subsonic de vòstre compte estant." -#: front/src/App.vue:137 +#: front/src/App.vue:162 src/components/About.vue:55 +#: src/components/Home.vue:154 front/src/components/PageNotFound.vue:33 +#: src/components/Sidebar.vue:203 front/src/components/Sidebar.vue:204 +#: src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 +#: src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 +#: src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 +#: src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 +#: src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 +#: src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 +#: src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 +#: src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 +#: src/components/audio/SearchBar.vue:27 front/src/components/auth/Login.vue:77 +#: src/components/auth/Login.vue:78 front/src/components/auth/Logout.vue:20 +#: src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 +#: src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 +#: src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 +#: src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 +#: src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 +#: src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 +#: src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 +#: src/views/auth/EmailConfirm.vue:51 front/src/views/auth/PasswordReset.vue:52 +#: src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +#: src/views/content/Base.vue:19 front/src/views/content/Home.vue:35 +#: src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 +#: src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 +#: src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 +#: src/views/instance/Timeline.vue:57 front/src/views/playlists/Detail.vue:90 +#: src/views/playlists/List.vue:104 front/src/views/playlists/List.vue:105 +#: src/views/radios/Detail.vue:80 msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "" -"Aquò escafarà vòstras donadas localas e vos desconnectarà, volètz contunhar ?" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "Aquò escafarà vòstras donadas localas e vos desconnectarà, volètz contunhar ?" +msgstr[1] "%{ count } pistas son ajustadas a la fila" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." -msgstr "" -"Aquò vos desconnectarà de totes los periferics qu’utilizan aqueste senhal." +msgstr "Aquò vos desconnectarà de totes los periferics qu’utilizan aqueste senhal." #: front/src/components/playlists/Editor.vue:44 msgid "This will remove all tracks from this playlist and cannot be undone." -msgstr "" -"Aquò escafarà totas las pistas de la lista de lectura e poirà pas èsser " -"anullat." +msgstr "Aquò escafarà totas las pistas de la lista de lectura e poirà pas èsser anullat." + +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." +msgstr "Aquò escafarà las pistas enviadas mas que lo servidor poguèt pas tractar. Aquò tirarà complètament los fichièrs e vos donarà de nòu lo quòta escafat." + +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "Aquò escafarà las pistas enviadas mas pas tractadas pel moment. Aquò tirarà los complètament los fichièrs e vos donarà de nòu lo quòta escafat." + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." +msgstr "Aquò escafarà las pistas enviadas mas ignoradas pendent lo processús d’importacion per mantun rasons. Aquò tirarà complètament los fichièrs e vos donarà de nòu lo quòta escafat." #: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 #: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 msgid "Title" msgstr "Títol" -#: front/src/components/audio/SearchBar.vue:27 -#: src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" -msgstr "Pista" - #: front/src/components/library/Track.vue:53 msgid "Track information" msgstr "Informacions de la pista" @@ -2231,15 +1866,11 @@ msgstr "pistas" #: front/src/components/library/Album.vue:43 #: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 -#: front/src/views/radios/Detail.vue:34 +#: src/views/content/Base.vue:8 front/src/views/content/libraries/Detail.vue:8 +#: src/views/playlists/Detail.vue:50 front/src/views/radios/Detail.vue:34 msgid "Tracks" msgstr "Pistas" -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "Pistas disponiblas dins aquesta bibliotèca" - #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" msgstr "Pistas d’aqueste artista" @@ -2252,36 +1883,36 @@ msgstr "Pistas en favorits" msgid "tracks listened" msgstr "pistas escotadas" -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "Aviar un analisi" - +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" msgstr "Tipe" +#: front/src/views/content/remote/Card.vue:100 +#: src/views/content/remote/Card.vue:105 +msgid "Unfollow" +msgstr "Quitar de seguir" + +#: front/src/views/content/remote/Card.vue:101 +msgid "Unfollow this library?" +msgstr "Quitar de seguir aquesta bibliotèca ?" + #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." -msgstr "" -"Malurosament, los gestionaris d’aquesta instància completèron pas aquesta " -"pagina." - -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "Desconegut" +msgstr "Malurosament, los gestionaris d’aquesta instància completèron pas aquesta pagina." #: front/src/components/Home.vue:37 msgid "Unlimited music" msgstr "Musica sens cap de limit" -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "Restablir lo son" - #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" msgstr "Actualizar l’avatar" +#: front/src/views/content/libraries/Form.vue:25 +msgid "Update library" +msgstr "Actualizar la bibliotèca" + #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" msgstr "Actualizar la lista de lectura" @@ -2294,8 +1925,10 @@ msgstr "Actualizar los paramètres" msgid "Update your password" msgstr "Actualizar vòstre senhal" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" msgstr "Mandadís" @@ -2303,45 +1936,51 @@ msgstr "Mandadís" msgid "Upload a new avatar" msgstr "Enviar un nòu avatar" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" -msgstr "Fichièrs enviats o font extèrna" +#: front/src/views/content/Home.vue:6 +msgid "Upload audio content" +msgstr "Actualizar lo contengut àudio" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/views/content/libraries/FilesTable.vue:54 +msgid "Upload date" +msgstr "Data de mandadís" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "Enviatz vòstres fichièrs musicals (mp3, ogg, flac, etc.) de vòstra bibliotèca personala estant dirèctament amb vòstre navigador per ne profechar aquí." + +#: front/src/components/library/FileUpload.vue:31 +msgid "Upload new tracks" +msgstr "Enviar nòvas pistas" + +#: front/src/views/admin/users/UsersDetail.vue:82 +msgid "Upload quota" +msgstr "Quòta de mandadís" + +#: front/src/components/library/FileUpload.vue:99 +msgid "Uploaded" +msgstr "Enviat" + +#: front/src/components/library/FileUpload.vue:5 +msgid "Uploading" +msgstr "Mandadís en cors" + +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "Mandadís en cors..." -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" msgstr "Utilizar una autra instància" -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "Utilizatz aqueste camp per ajustar de detalhs a vòstra demanda, se cal" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "" -"Utilizatz aqueste paramètre per activar/desactivar la deferacion amb aquesta " -"bibliotèca" - #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." -msgstr "" -"Garnissètz aqueste formulari per demandar un nòu senhal. Auretz un corrièl a " -"vòstra adreça indicada contenent las consignas de reïnicializacion." - -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "" -"Utilizatz aqueste formulari per analizar lo catalòg d’una instància e " -"establir la federacion." +msgstr "Garnissètz aqueste formulari per demandar un nòu senhal. Auretz un corrièl a vòstra adreça indicada contenent las consignas de reïnicializacion." #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" msgstr "Utilizat" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" msgstr "Utilizaire" @@ -2349,12 +1988,19 @@ msgstr "Utilizaire" msgid "User activity" msgstr "Activitat dels utilizaires" +#: front/src/components/library/Album.vue:49 +#: src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +msgid "User libraries" +msgstr "Bibliotècas de l’utilizaire" + #: front/src/components/library/Radios.vue:20 msgid "User radios" msgstr "Ràdios dels utilizaires" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" msgstr "Nom d’utilizaire" @@ -2366,21 +2012,30 @@ msgstr "Nom d’utilizaire o corrièl" msgid "users" msgstr "utilizaires" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 +#: src/views/admin/users/UsersList.vue:21 front/src/views/admin/Settings.vue:81 msgid "Users" msgstr "Utilizaires" +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +msgid "View files" +msgstr "Veire los fichièrs" + #: front/src/components/library/Album.vue:37 #: src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" msgstr "Veire sus MusicBrainz" +#: front/src/views/content/libraries/Form.vue:18 +msgid "Visibility" +msgstr "Visibilitat" + #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" msgstr "Podèm pas ajustar aquesta pista a una lista de lectura" @@ -2405,13 +2060,21 @@ msgstr "Podèm pas enregistrar vòstre avatar" msgid "We cannot save your settings" msgstr "Podèm pas enregistrar vòstres paramètres" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" msgstr "Vos pistam pas e vos mostram pas cap de reclama" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." -msgstr "Vos recomandam d’utilizar lo logicial Picard per aquò far." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "Avèm pas cap de notificacion de vos far veire !" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." +msgstr "Prepausam mantun biais de recuperar de nòu contengut e de lo far venir disponible aquí." + +#: front/src/components/library/FileUpload.vue:40 +msgid "we recommend using Picard for that purpose" +msgstr "vos recomandam d’utilizar lo logicial Picard per aquò far" #: front/src/components/Home.vue:7 msgid "We think listening to music should be simple." @@ -2425,29 +2088,15 @@ msgstr "O planhèm, la pagina demandada existís pas :" msgid "We've received your request, you'll get some groove soon ;)" msgstr "Avèm ben recebut vòstra requèsta, aurètz lèu de novèlas ;)" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "La benvenguda" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" msgstr "La benvenguda a Funkwhale" -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "Qu’es aquò las metadonadas ?" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "" -"S’aqueste paramètre es activat, las novèlas pistas ajustadas a aquesta " -"bibliotèca seràn automaticament importadas" - #: front/src/components/Home.vue:24 msgid "Why funkwhale?" msgstr "Perque Funkwhale ?" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" msgstr "Òc" @@ -2455,82 +2104,468 @@ msgstr "Òc" msgid "Yes, log me out!" msgstr "Òc-ben, desconnectatz-me !" +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" +msgstr "Sètz a man d’enviar de la musica a vòstra bibliotèca. Abans de començar, mercés de vos assegurar que :" + #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" msgstr "Sètz connectat coma %{ username }" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." -msgstr "Podètz passar aquesta etapa e dintrar las metadonadas manualament." +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." +msgstr "Podètz seguir las bibliotècas d’autres utilizaires per accedir a de nòvas musicas. Las bibliotècas publicas pòdon èsser seguidas còp sec, mentre qu’una bibliotèca privada demanda una aprovacion de sus proprietaris." -#: front/src/components/Home.vue:136 +#: front/src/components/Home.vue:133 msgid "You can invite friends and family to your instance so they can enjoy your music" -msgstr "" -"Podètz convidar vòstres amics e vòstra familha a aquesta instància per que " -"pòscan profeitar de vòstra musica" +msgstr "Podètz convidar vòstres amics e vòstra familha a aquesta instància per que pòscan profeitar de vòstra musica" #: front/src/components/library/radios/Builder.vue:7 msgid "You can use this interface to build your own custom radio, which will play tracks according to your criteria." -msgstr "" -"Podètz utilizar aquesta interfàcia per realizar vòstra pròpria ràdio " -"personalizada, que jogarà las listas segon los critèris indicats." +msgstr "Podètz utilizar aquesta interfàcia per realizar vòstra pròpria ràdio personalizada, que jogarà las listas segon los critèris indicats." #: front/src/components/auth/SubsonicTokenForm.vue:8 msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." -msgstr "" -"Podètz los utilizar per profeitar de vòstras listas de lectura e de vòstra " -"musica en mòde fòra linha sus vòstre mobil, tableta, per exemple." +msgstr "Podètz los utilizar per profeitar de vòstras listas de lectura e de vòstra musica en mòde fòra linha sus vòstre mobil, tableta, per exemple." -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" msgstr "Escotatz una ràdio" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" msgstr "Vos cal causir una instància per contunhar" +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." +msgstr "Poiretz partejar vòstra bibiotèca amb d’autres gents, sens importància de sa visibilitat." + #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" -msgstr "" -"Sètz per èsser desconnectat d’aquesta session e vos caldrà vos connectar amb " -"lo nòu senhal" +msgstr "Sètz per èsser desconnectat d’aquesta session e vos caldrà vos connectar amb lo nòu senhal" #: front/src/components/auth/Settings.vue:71 msgid "You will have to update your password on your clients that use this password." -msgstr "" -"Vos caldrà actualizar lo senhal sus totes los clients qu’utilizan aqueste " -"senhal." - -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "Sètz per importar :" +msgstr "Vos caldrà actualizar lo senhal sus totes los clients qu’utilizan aqueste senhal." #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." -msgstr "" -"Vòstra adreça electronica es confirmada, podètz ara utilizar lo servici sens " -"cap de limitacions." +msgstr "Vòstra adreça electronica es confirmada, podètz ara utilizar lo servici sens cap de limitacions." -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" -msgstr "Vòstres favorits" - -#: front/src/components/Home.vue:117 +#: front/src/components/Home.vue:114 msgid "Your music, your way" msgstr "Vòstra musica, coma volètz" +#: front/src/views/Notifications.vue:7 +msgid "Your notifications" +msgstr "Vòstras notificacions" + #: front/src/views/auth/PasswordResetConfirm.vue:29 msgid "Your password has been updated successfully." msgstr "Vòstre senhal es corrèctament cambiat." #: front/src/components/auth/Settings.vue:101 msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" -msgstr "" -"Vòstre senhal Subsonic serà remplaçat per un nòu aleatòri, aquò vos " -"desconnectarà de totes los periferics qu’utilizan l’ancian senhal" +msgstr "Vòstre senhal Subsonic serà remplaçat per un nòu aleatòri, aquò vos desconnectarà de totes los periferics qu’utilizan l’ancian senhal" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +msgid "Activity visibility" +msgstr "Visibilitat de l’activitat" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "Determinatz lo nivèl de visibilitat de vòstra activitat" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 +#: src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "Degun fòra ieu" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 +#: src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "Lo monde d’aquesta instància" + +#: front/src/components/mixins/Translations.vue:18 +msgid "Accessed date" +msgstr "Data d’accès" + +#: front/src/components/mixins/Translations.vue:19 +msgid "Modification date" +msgstr "Data de modificacion" + +#: front/src/components/mixins/Translations.vue:20 +msgid "Imported date" +msgstr "Data d’import" + +#: front/src/components/mixins/Translations.vue:22 +msgid "Track name" +msgstr "Nom de la pista" + +#: front/src/components/mixins/Translations.vue:23 +msgid "Album name" +msgstr "Nom de l’album" + +#: front/src/components/mixins/Translations.vue:30 +msgid "Sign-up date" +msgstr "Data d’inscripcion" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "Copiar las pistas de la fila a la lista de lectura" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "Ajustar a aquesta lista de lectura" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "Ma lista de lectura tròp crana" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "Tot lo monde" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "Inscripcion" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "Escrivètz vòstre còdi d’invitacion (pas sensible a la cassa)" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "Escrivètz vòstre nom d’utilizaire" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "Escrivètz vòstre adreça electronica" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "Senhal actualizat" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "Accès desactivat" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "Escrivètz vòstre nom d’utilizaire o corrièl" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "Connexion" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "Perfil de %{ username }" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "Desconnexion" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "Paramètres del compte" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "Tirar dels favorits" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "Vòstres favorits" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "Escrivètz un nom de ràdio..." + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "Editor de ràdio" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "Ma ràdio tròp crana" + +#: front/src/components/library/radios/Builder.vue:236 +msgid "My awesome description" +msgstr "Ma descripcion tròp crana" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "Mandadís refusat, asseguratz-vos que lo fichièr es pas tròp grand e qu’avètz pas atenhut vòstre quòta" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "Mandadís del fichièr impossible, asseguratz-vos qu’es pas tròp pesuc" + +#: front/src/components/library/FileUpload.vue:240 +msgid "A network error occured while uploading this file" +msgstr "Una error de ret s’es producha en enviar aqueste fichièr" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "Temps de transferiment expirat, ensajatz tornamai" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "Escrivètz un nom d’artista..." + +#: front/src/components/library/Track.vue:195 +#: src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "Pista" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "Acuèlh" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "Mostrar/amagar lo senhal" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "Los Beatles, Mickael Jackson…" + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "L'Album Blanc, Thriller…" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "Utilizatz aqueste camp per ajustar de detalhs a vòstra demanda, se cal" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" msgstr[0] "%{ count } pista ajustada a la fila" msgstr[1] "%{ count } pistas ajustadas a la fila" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "Artista, album, pista..." + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "Recercar d’artistas, d’albums, de pistas..." + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "La fila es estada mesclada !" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "Pista precedenta" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "Legir" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "Pausar" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "Pista seguenta" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "Restablir lo son" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "Copar lo son" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "Repeticion desactivada. Clicatz per activar la repeticion de la pista actuala." + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "Repeticion de la pista actuala. Clicatz per activar la repeticion de tota la fila." + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "Repeticion de tota la fila, clicatz per desactivar la repeticion." + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "Mesclar la fila" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "Voidar la fila" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "Requèstas en espèra" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "Demandas d’abonament en espèra" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "Escrivètz vòstra recèrca..." + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "Recercar per títol, artista, domeni..." + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "Daissar void per obténer un còdi aleatòri" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "Recercar per nom d’utilizaire, corrièl, còdi..." + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "Recercar per nom d’utilizaire, corrièl, nom..." + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "Federacion" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "La benvenguda" + +#: front/src/views/content/remote/ScanForm.vue:48 +msgid "Enter a library url" +msgstr "Picatz l’URL d’una bibliotèca" + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "Exploracion lançada" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "Exploracion sautada (la darrièra es tròp recenta)" + +#: front/src/views/content/libraries/FilesTable.vue:226 +msgid "Search by title, artist, album..." +msgstr "Recercar per títol, artista, album..." + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "La pista ja presenta dins una de vòstras bibliotècas" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "La pista es enviada mas pas encara tractada pel servidor" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "Una error s’es producha en tractar aquesta pista, asseguratz-vos qu’es corrèctament etiquetada" + +#: front/src/views/content/libraries/FilesTable.vue:242 +msgid "Import went on successfully" +msgstr "L’importacion a reüssit" + +#: front/src/views/content/libraries/FilesTable.vue:259 +msgid "Relaunch import" +msgstr "Relançar l’import" + +#: front/src/views/content/libraries/Card.vue:58 +msgid "Visibility: nobody except me" +msgstr "Visibilitat : degun levat ieu" + +#: front/src/views/content/libraries/Card.vue:59 +msgid "Visibility: everyone on this instance" +msgstr "Visibilitat : lo monde d’aquesta instància" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "Visibilitat : totes, tanben las autras instàncias" + +#: front/src/views/content/libraries/Card.vue:61 +msgid "Total size of the files in this library" +msgstr "Talha totala dels fichièrs d’aquesta bibliotèca" + +#: front/src/views/content/libraries/Form.vue:70 +msgid "My awesome library" +msgstr "Ma bibliotèca tròp crana" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "Aquesta bibliotèca conten ma musica personala, espèri que vos agradarà !" + +#: front/src/views/content/libraries/Form.vue:74 +msgid "Everyone, including other instances" +msgstr "Tot lo monde, d’autras instàncias tanben" + +#: front/src/views/content/libraries/Form.vue:106 +msgid "Library updated" +msgstr "Bibliotèca actualizada" + +#: front/src/views/content/libraries/Form.vue:109 +msgid "Library created" +msgstr "Bibliotèca creada" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "Ajustar e gerir lo contengut" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "Ràdio" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "Flux de l’instància" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "Lista de lectura" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "Escrivètz un nom de lista de lectura..." + +#: front/src/views/admin/library/Base.vue:16 +msgid "Manage library" +msgstr "Gerir la bibliotèca" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "Determina se l’utilizaire es actiu o non. Los compte inactius se pòdon pas connectar nimai utilizar lo servici." + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "Definissètz la quantitat de contengut que l’utilizaire pòt enviar. Daissatz void per emplegar las valors per defaut de l’instància." + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "Gerir los utilizaires" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "Paramètres de l’intància" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "Info. de l’instància" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "Subsonic" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "Estatisticas" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "Rapòrt d’error" diff --git a/front/locales/pl/LC_MESSAGES/app.po b/front/locales/pl/LC_MESSAGES/app.po index 998f4e61e..33c89a1fd 100644 --- a/front/locales/pl/LC_MESSAGES/app.po +++ b/front/locales/pl/LC_MESSAGES/app.po @@ -7,16 +7,15 @@ msgid "" msgstr "" "Project-Id-Version: front 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-22 14:12+0200\n" -"PO-Revision-Date: 2018-07-31 13:32+0000\n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" +"PO-Revision-Date: 2018-10-02 17:46+0000\n" "Last-Translator: Marcin Mikołajczak \n" "Language-Team: \n" "Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " -"|| n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 2.20\n" #: front/src/components/playlists/PlaylistModal.vue:9 @@ -31,16 +30,17 @@ msgstr "(%{ index } z %{ length })" msgid "(empty)" msgstr "(pusta)" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "%{ count } z %{ total } zaznaczonego" msgstr[1] "%{ count } z %{ total } zaznaczonych" msgstr[2] "%{ count } z %{ total } zaznaczonych" -#: front/src/components/Sidebar.vue:116 -#: src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 +#: src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" msgstr[0] "%{ count } utwór" @@ -54,7 +54,7 @@ msgstr[0] "%{ count } utwór w %{ albumsCount } albumach" msgstr[1] "%{ count } utwory w %{ albumsCount } albumach" msgstr[2] "%{ count } utworow w %{ albumsCount } albumach" -#: front/src/components/library/radios/Builder.vue:76 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" msgstr[0] "%{ count } utwór zgodny z wybranymi filtrami" @@ -68,6 +68,10 @@ msgstr[0] "%{ count} utwór" msgstr[1] "%{ count} utwory" msgstr[2] "%{ count} utworów" +#: front/src/views/content/libraries/Quota.vue:11 +msgid "%{ current } used on %{ max } allowed" +msgstr "Wykorzystano %{ count } z dozwolonego %{ total }" + #: front/src/components/common/Duration.vue:2 msgid "%{ hours } h %{ minutes } min" msgstr "%{ hours } godz. %{ minutes } min" @@ -84,10 +88,6 @@ msgstr "%{ user } dodał utwór do ulubionych" msgid "%{ user } listened to a track" msgstr "%{ user } słuchał utworu" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "Profil %{ username }" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" @@ -102,36 +102,27 @@ msgstr[0] "1 polubienie" msgstr[1] "%{ count} polubienia" msgstr[2] "%{ count} polubień" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "1 utwór" -msgstr[1] "%{ count } utwory" -msgstr[2] "%{ count } utworów" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "O %{ instance }" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "O Funkwhale" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "O tej instancji" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 +#: front/src/views/content/libraries/Detail.vue:48 +msgid "Accept" +msgstr "Zaakceptuj" + +#: front/src/views/content/libraries/Detail.vue:40 msgid "Accepted" msgstr "Zaakceptowano" -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "Brak dostępu" - -#: front/src/components/Home.vue:109 +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" msgstr "Uzyskaj dostęp do swojej muzyki z przejrzystego interfejsu skupionego na tym, co naprawdę ważne" @@ -143,10 +134,6 @@ msgstr "Konto aktywne" msgid "Account settings" msgstr "Ustawienia konta" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "Ustawienia konta" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "Stan konta" @@ -155,7 +142,11 @@ msgstr "Stan konta" msgid "Account's email" msgstr "Adres e-mail konta" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +msgid "Action" +msgstr "Działanie" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" msgstr[0] "Działanie %{ action } zostało rozpoczęte pomyślnie na %{ count } elemencie" @@ -163,9 +154,7 @@ msgstr[1] "Działanie %{ action } zostało rozpoczęte pomyślnie na %{ count } msgstr[2] "Działanie %{ action } zostało rozpoczęte pomyślnie na %{ count } elementach" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:60 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "Działania" @@ -173,30 +162,22 @@ msgstr "Działania" msgid "Active" msgstr "Aktywny" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "Aktywność" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" -msgstr "Aktor" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" +msgstr "Dodaj zawartość" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "Dodaj nową bibliotekę" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" msgstr "Dodaj filtr" -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" msgstr "Dodaj filtry aby dostosować swoje radio" -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "Dodaj do obecnej kolejki" - #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" @@ -211,10 +192,6 @@ msgstr "Dodaj do playlisty…" msgid "Add to queue" msgstr "Dodaj do kolejki" -#: front/src/components/playlists/PlaylistModal.vue:116 -msgid "Add to this playlist" -msgstr "Dodaj do tej playlisty" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "Dodaj utwór" @@ -223,26 +200,19 @@ msgstr "Dodaj utwór" msgid "Admin" msgstr "Administrator" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" msgstr "Administracja" -#: front/src/components/audio/SearchBar.vue:26 -#: src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 +#: src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "Album" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "Album %{ title } (%{ count } utwór) od %{ artist }" -msgstr[1] "Album %{ title } (%{ count } utwory) od %{ artist }" -msgstr[2] "Album %{ title } (%{ count } utworów) od %{ artist }" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" @@ -256,7 +226,6 @@ msgstr "Strona albumu" #: front/src/components/audio/Search.vue:19 #: src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "Albumy" @@ -265,8 +234,8 @@ msgstr "Albumy" msgid "Albums by this artist" msgstr "Albumy tego wykonawcy" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "Wszystkie" @@ -278,45 +247,21 @@ msgstr "Wystąpił błąd podczas zapisywania zmian" msgid "An unknown error happend, this can mean the server is down or cannot be reached" msgstr "Wystąpił nieznany błąd, może oznaczać to że serwer jest wyłączony lub nieosiągalny" -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "Dowolne" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "API" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "Zaakceptuj" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "Pozwolić na dostęp?" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "Pozwolono" - #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "Czy na pewno chcesz się wylogować?" -#: front/src/components/audio/SearchBar.vue:25 -#: src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 +#: src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" msgstr "Wykonawca" #: front/src/components/requests/Form.vue:5 +#: src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "Nazwa wykonawcy" @@ -325,26 +270,20 @@ msgstr "Nazwa wykonawcy" msgid "Artist page" msgstr "Strona wykonawcy" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "Wykonawca, album, utwór…" - #: front/src/components/audio/Search.vue:10 #: src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 -#: src/components/library/Library.vue:7 +#: front/src/components/library/Library.vue:7 +#: src/components/library/Artists.vue:120 msgid "Artists" msgstr "Wykonawcy" #: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 -#: src/components/library/Radios.vue:44 +#: src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 -#: src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" msgstr "Rosnąco" @@ -352,10 +291,6 @@ msgstr "Rosnąco" msgid "Ask for a password reset" msgstr "Poproś o zresetowanie hasła" -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "Automatyczne importowanie" - #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" msgstr "Dostępne playlisty" @@ -369,12 +304,9 @@ msgstr "Awatar" msgid "Back to login" msgstr "Wróć do logowania" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "Zachowaj ostrożność akceptując prośby o możliwość śledzenia, ponieważ śledzący będzie miał dostęp do całej Twojej biblioteki." - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "Bitrate" @@ -382,7 +314,7 @@ msgstr "Bitrate" msgid "Browse" msgstr "Przeglądaj" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "Przeglądaj bibliotekę" @@ -390,18 +322,6 @@ msgstr "Przeglądaj bibliotekę" msgid "Browsing artists" msgstr "Przeglądanie wykonawców" -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "Przeglądanie sfederowanych utworów" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "Przeglądanie śledzących" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "Przeglądanie bibliotek" - #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" msgstr "Przeglądanie list odtwarzania" @@ -418,22 +338,17 @@ msgstr "Budowanie" msgid "By %{ artist }" msgstr "Od %{ artist }" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." -msgstr "Po potwierdzeniu %{ username } nie będzie miał dostępu do Twojej biblioteki." +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." +msgstr "" -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "Po potwierdzeniu %{ username } uzyska dostęp do Twojej biblioteki." - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" msgstr "Anuluj" -#: front/src/components/library/radios/Builder.vue:59 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "Kandydaci" @@ -441,7 +356,7 @@ msgstr "Kandydaci" msgid "Cannot change your password" msgstr "Nie udało się zmienić Twojego hasła" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" msgstr "Zmień język" @@ -474,7 +389,7 @@ msgstr "ZMiana Twojego hasła zmieni też Twoje hasło API Subsonic, jeżeli uzy msgid "Changing your password will have the following consequences" msgstr "Zmiana hasła będzie miała następujące konsekwencje" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "Wybierz instancję" @@ -491,22 +406,13 @@ msgstr "Wyczyść" msgid "Clear playlist" msgstr "Wyczyść listę odtwarzania" -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "Wyczyść swoją kolejkę" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "CLI" - #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" msgstr "Naciśnij raz, słuchaj godzinami dzięki wbudowanemu radio" -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" -msgstr "Zamknięte" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" +msgstr "Naciśnij aby wybrać pliki do wysłania lub przeciągnij i upuść pliki lub katalogi" #: front/src/components/manage/users/InvitationForm.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:42 @@ -519,12 +425,11 @@ msgstr "Kod" msgid "Collapse" msgstr "Zwiń" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "Komentarz" -#: front/src/components/library/radios/Builder.vue:58 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" msgstr "Konfiguracja" @@ -541,11 +446,11 @@ msgstr "Potwierdź swój e-mail" msgid "Confirmation code" msgstr "Kod potwierdzający" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" -msgstr "Skopiuj utwory z obecnej kolejki do listy odtwarzania" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" +msgstr "Kopiuj" -#: front/src/components/Home.vue:88 +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" msgstr "Covery, teksty, naszym celem jest mieć je wszystkie ;)" @@ -553,19 +458,26 @@ msgstr "Covery, teksty, naszym celem jest mieć je wszystkie ;)" msgid "Create a funkwhale account" msgstr "Utwórz konto funkwhale" +#: front/src/views/content/libraries/Home.vue:14 +msgid "Create a new library" +msgstr "Utwórz nową bibliotekę" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "Utwórz nową listę odtwarzania" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "Utwórz konto" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "Utwórz importowanie" +#: front/src/views/content/libraries/Form.vue:26 +msgid "Create library" +msgstr "Utwórz bibliotekę" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "Utwórz konto" @@ -578,9 +490,8 @@ msgstr "Utwórz listę odtwarzania" msgid "Create your own radio" msgstr "Utwórz własne radio" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "Data utworzenia" @@ -588,17 +499,34 @@ msgstr "Data utworzenia" msgid "Current avatar" msgstr "Obecny awatar" +#: front/src/views/content/libraries/DetailArea.vue:4 +msgid "Current library" +msgstr "Obecna biblioteka" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "Obecny utwór" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +msgid "Current usage" +msgstr "Obecne użycie" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "Data" + +#: front/src/views/content/libraries/Form.vue:29 +#: src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "Usuń" +#: front/src/views/content/libraries/Form.vue:39 +msgid "Delete library" +msgstr "Usuń bibliotekę" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "Usuń listę odtwarzania" @@ -607,34 +535,33 @@ msgstr "Usuń listę odtwarzania" msgid "Delete radio" msgstr "Usuń radio" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" -msgstr "Odmów" - -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "Odmówić dostępu?" +#: front/src/views/content/libraries/Form.vue:31 +msgid "Delete this library?" +msgstr "Czy chcesz usunąć tę bibliotekę?" #: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 -#: src/components/library/Radios.vue:47 +#: src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 -#: src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "Malejąco" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +msgid "Description" +msgstr "Opis" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "Szczegół" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." -msgstr "Określa, czy konto użytkownika jest aktywne. Nieaktywni użytkownicy nie mogą zalogować się i korzystać z usługi." +#: front/src/views/content/remote/Card.vue:50 +#, fuzzy +msgid "Details" +msgstr "Szczegół" #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 @@ -653,7 +580,7 @@ msgstr "Wyłączyć dostęp do API Subsonic?" msgid "Discover how to use Funkwhale from other apps" msgstr "Odkryj, jak korzystać z Funkwhale z innych aplikacji" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "Wyświetlaj publicznie" @@ -680,41 +607,43 @@ msgstr[0] "Czy chcesz wykonać działanie %{ action } na %{ count } elemencie?" msgstr[1] "Czy chcesz wykonać działanie %{ action } na %{ count } elementach?" msgstr[2] "Czy chcesz wykonać działanie %{ action } na %{ count } elementach?" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "Czy chcesz przywrócić poprzednią kolejkę?" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "Dokumentacja" -#: front/src/components/audio/track/Table.vue:24 -#: src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "Pobierz" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "Pobierz utwory" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" msgstr "Przeciągnij i upuść aby zmienić kolejność utworów w liście odtwarzania" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 +#: src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "Długość" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "Prosty w użyciu" +#: front/src/views/content/libraries/Detail.vue:9 +msgid "Edit" +msgstr "Edytuj" + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "Edytuj informacje o instancji" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "Edytuj…" @@ -735,55 +664,17 @@ msgstr "Potwierdzono e-mail" msgid "End edition" msgstr "Zakończ edytowanie" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "Upewnij się, że pliki muzyczne są odpowiednio otagowane zanim je wyślesz." - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "Wprowadź nazwę radia…" - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "Wprowadź nazwę wykonawcy…" - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "Wprowadź nazwę domeny biblioteki…" - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "Wprowadź nazwę listy odtwarzania…" - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "Wprowadź swój e-mail" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "Wprowadź swój kod zapraszający (wielkość znaków nie ma znaczenia)" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "Wprowadź swoje kryterium wyszukiwania…" - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "Wprowadź swoją nazwę użytkownika" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "Wprowadź swoją nazwę użytkownika lub e-mail" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "Błąd" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" +#: front/src/views/content/remote/Card.vue:39 +#, fuzzy +msgid "Error during scan" msgstr "Zgłaszanie błędów" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "Błąd podczas zastosowywania działania" @@ -803,29 +694,30 @@ msgstr "Wystąpił błąd podczas potwierdzania adresu e-mail" msgid "Error while creating invitation" msgstr "Wystąpił błąd podczas tworzenia zaproszenia" +#: front/src/views/content/remote/ScanForm.vue:3 +msgid "Error while fetching remote library" +msgstr "Wystąpił błąd podczas uzyskiwania zdalnej biblioteki" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "Wystąpił błąd podczas zapisywania ustawień" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "Wystąpił błąd podczas przeszukiwania biblioteki" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "Zakończono błędem" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" -msgstr "Wszyscy" +#: front/src/views/content/libraries/Quota.vue:75 +#, fuzzy +msgid "Errored files" +msgstr "Zakończono błędem" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" -msgstr "Wszyscy na tej instancji" +#: front/src/views/content/remote/Card.vue:58 +#, fuzzy +msgid "Errored tracks:" +msgstr "Sfederowane utwory" -#: front/src/components/library/radios/Builder.vue:57 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "Wyłącz" @@ -834,6 +726,7 @@ msgid "Expand" msgstr "Rozwiń" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "Data wygaśnięcia" @@ -845,97 +738,51 @@ msgstr "Wyczerpany" msgid "Expired/used" msgstr "Wyczerpany/zużyty" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "Zewnętrzne źródło. Obsługiwane back-endy" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "Ulubione" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "Federuj z nową instancją" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "Sfederowane utwory" - -#: front/src/components/Sidebar.vue:87 -#: src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 -#: src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 -#: src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "Federacja" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "Powielanie plików" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "Nazwa pliku" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "Wysyłanie pliku" - #: front/src/views/admin/library/Base.vue:5 #: src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "Pliki" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "Filtruj rodzaje albumów" - -#: front/src/components/library/radios/Builder.vue:56 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "Filtruj nazwę" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "Zakończ importowanie" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "Ukończono" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "Najpierw wybierz, skąd chcesz zaimportować muzykę" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "Śledź" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" +#: front/src/views/content/remote/Card.vue:88 +#, fuzzy +msgid "Follow pending approval" msgstr "Prośba o możliwość śledzenia oczekuje na zatwierdzenie" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" -msgstr "Stan śledzenia" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" +msgstr "Śledź zdalne biblioteki" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "Śledzący" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "Tylko dla śledzących" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "Śledzisz" -#: front/src/components/activity/Like.vue:12 -#: src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 +#: src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "z %{ album } od %{ artist }" @@ -943,7 +790,7 @@ msgstr "z %{ album } od %{ artist }" msgid "From album %{ album } by %{ artist }" msgstr "Z albumu %{ album } od %{ artist }" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" msgstr "Funkwhale jest wolnym i otwartoźródłowym projektem rozwijanym przez wolontariuszy. Możesz pomóc nam ulepszać platformę zgłaszając błędy, propozycje funkcji i polecając projekt znajomym!" @@ -951,7 +798,7 @@ msgstr "Funkwhale jest wolnym i otwartoźródłowym projektem rozwijanym przez w msgid "Funkwhale is compatible with other music players that support the Subsonic API." msgstr "Funkwhale jest kompatybilny z innymi odtwarzaczami muzycznymi obsługującymi API Subsonic." -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "Funkwhale jest niezwykle prosty w użyciu." @@ -959,7 +806,7 @@ msgstr "Funkwhale jest niezwykle prosty w użyciu." msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." msgstr "Funkwhale został zaprojektowany, aby uczynić słuchanie muzyki którą lubisz i poznawanie nowych wykonawców prostym." -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." msgstr "Funkwhale jest wolny i oddaje Ci kontrolę nad Twoją muzyką." @@ -975,18 +822,19 @@ msgstr "Uzyskaj nowe zaproszenie" msgid "Get me to the library" msgstr "Pokaż mi bibliotekę" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " +#: front/src/components/Home.vue:76 +#, fuzzy +msgid "Get quality metadata about your music thanks to MusicBrainz" msgstr "" "Uzyskaj dobrej jakości metadane o Twojej muzyce dzięki\n" " \n" " MusicBrainz\n" " " +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +msgid "Get started" +msgstr "Rozpocznij" + #: front/src/components/common/ActionTable.vue:21 #: front/src/components/common/ActionTable.vue:27 msgid "Go" @@ -996,18 +844,10 @@ msgstr "Przejdź" msgid "Go to home page" msgstr "Przejdź na stronę główną" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "Uzyskaj odpowiednie metadane" - -#: front/src/App.vue:74 +#: front/src/App.vue:73 msgid "Help us translate Funkwhale" msgstr "Pomóż nam tłumaczyć Funkwhale" -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "Strona główna" - #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" msgstr "Godziny muzyki" @@ -1016,129 +856,36 @@ msgstr "Godziny muzyki" msgid "However, accessing Funkwhale from those clients require a separate password you can set below." msgstr "Korzystanie z Funkwhale z tych klientów wymaga jednak oddzielnego hasła, które możesz ustawić poniżej." -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "ID" - #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." msgstr "Jeżeli adres e-mail podany w poprzednim kroku jest prawidłowy i przypisany do konta użytkownika, powinieneś dostać wiadomość z instrukcjami resetowania hasła w przeciągu kilku minut." -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "Importuj" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "Rozpoczęto importowanie #%{ id }" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "Importuj %{ count } utwór" -msgstr[1] "Importuj %{ count } utwory" -msgstr[2] "Importuj %{ count } utwów" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "Importowanie wsadowe" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "Importowanie wsadowe #%{ id }" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "Importuj wsadowo" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "Importowania wsadowe" - #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "Data zaimportowania" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "Strona szczegółów importowania" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "Importuj muzykę" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "Importuj muzykę" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" msgstr "Importuj muzykę z różnych platform, takich jak YouTube i SoundCloud" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" -msgstr "Oczekujące importowanie" - -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "Prośby o zaimportowanie" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" +#: front/src/components/library/FileUpload.vue:51 +#, fuzzy +msgid "Import reference" msgstr "Źródło importu" -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "Stan importu" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "Importuj to wydanie" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "Importuj ten utwór" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "Zaimportowano" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "Zaimportowany URL" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "Importy" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "W ulubionych" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "W bibliotece" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "Nieaktywny" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "Wprowadź ID MusicBrainz ręcznie:" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "Wprowadź adres e-mail przypisany do Twojego konta" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" @@ -1146,22 +893,10 @@ msgstr[0] "Dodaj z kolejki (%{ count } utwór)" msgstr[1] "Dodaj z kolejki (%{ count } utwory)" msgstr[2] "Dodaj z kolejki (%{ count } utworów)" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "Informacje o instancji" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "Radia instancji" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "Ustawienia instancji" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "Oś czasu instancji" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1177,49 +912,42 @@ msgstr "Kod zapraszający (nieobowiązkowy)" msgid "Invitations" msgstr "Zaproszenia" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "Śledzenie błędów" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "ID zadania" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" -msgstr "Zadania" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" +msgstr "Wygląda na to, że nie masz jeszcze żadnej biblioteki — czas na jej utworzenie!" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" msgstr "Zachowaj kontrolę nad ulubionymi utworami" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." -msgstr "Zachowaj swój PRIVATE_TOKEN dla siebie, ponieważ daje on dostęp do Twojego konta." +#: front/src/views/content/remote/Home.vue:14 +msgid "Known libraries" +msgstr "Znane biblioteki" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "Ostatnia aktywność" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "Ostatnio dodane" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "Ostatnia modyfikacja" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +#, fuzzy +msgid "Last update:" +msgstr "Zaktualizowano listę odtwarzania" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "Uruchom" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "Data uruchomienia" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" msgstr "Uruchom skanowanie" @@ -1227,25 +955,21 @@ msgstr "Uruchom skanowanie" msgid "Learn more about this instance" msgstr "Dowiedz się więcej o tej instancji" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "Pozostaw puste, aby wygenerować kod" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "Pozostaw to pole puste, jeżeli chcesz całą dyskografię." -#: front/src/views/federation/Base.vue:5 -#: src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "Biblioteki" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "Biblioteki pomagają organizować i udostępniać kolekcje muzyki. Możesz wysłać własną kolekcję muzyki na Funkwhale i dzielić się nią z rodziną i znajomymi." + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "Biblioteka" @@ -1253,154 +977,111 @@ msgstr "Biblioteka" msgid "Library files" msgstr "Pliki z biblioteki" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "Nazwa biblioteki" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "Rozmiar biblioteki" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "library@demo.funkwhale.audio" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "Odnośniki" +#: front/src/views/content/libraries/Detail.vue:21 +msgid "Loading followers..." +msgstr "Ładowanie śledzących…" + +#: front/src/views/content/libraries/Home.vue:3 +msgid "Loading Libraries..." +msgstr "Ładowanie bibliotek…" + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +msgid "Loading library data..." +msgstr "Ładowanie danych biblioteki…" + +#: front/src/views/Notifications.vue:4 +msgid "Loading notifications..." +msgstr "Ładowanie powiadomień…" + +#: front/src/views/content/remote/Home.vue:3 +msgid "Loading remote libraries..." +msgstr "Ładowanie zdalnych bibliotek…" + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "Ładowanie osi czasu…" +#: front/src/views/content/libraries/Quota.vue:4 +msgid "Loading usage data..." +msgstr "Ładowanie danych o użyciu…" + #: front/src/components/favorites/List.vue:5 msgid "Loading your favorites..." msgstr "Ładowanie Twoich ulubionych…" -#: front/src/components/auth/Login.vue:78 -msgid "Log In" -msgstr "Zaloguj się" - #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "Zaloguj się na swoje konto Funkwhale" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "Wyloguj się" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "Zalogowano jako %{ username }" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "Logowanie" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "Wyloguj się" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "Zapętlanie jest wyłączone. Naciśnij, aby przełączyć na powtarzanie jednego utworu." - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "Zapętlanie jednego utworu jest włączone. Naciśnij, aby przełączyć na powtarzanie całej kolejki." - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "Zapętlanie całej kolejki jest włączone. Naciśnij, aby wyłączyć zapętlanie." - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "Tekst" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "Zarządzaj biblioteką" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "Zarządzaj listami odtwarzania" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "Zarządzaj użytkownikami" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "Zarządzaj swoimi listami odtwarzania" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" -msgstr "Oznacz jako zamkniętą" +#: front/src/views/Notifications.vue:17 +msgid "Mark all as read" +msgstr "Oznacz wszystko jako przeczytane" -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" -msgstr "Oznacz jako zaimportowaną" - -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "Metadane" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" msgstr "" -"Metadane to dane związane z muzyką, którą chcesz zaimportować. Zaliczają się do nicj wszystkie dane o wykonawcy, albumach i utworach. Aby uzyskać dopracowaną bibliotekę, polecamy uzyskać dane z projektu\n" -" \n" -" MusicBrainz\n" -" ,\n" -" który jest czymś w stylu Wikipedii dla muzyki." -#: front/src/components/Sidebar.vue:48 -#: src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "Muzyka" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "Prośba o muzykę" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "Wycisz" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "Moje konto" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" -msgstr "Moja wspaniała playlista" +#: front/src/views/content/libraries/Home.vue:6 +msgid "My libraries" +msgstr "Moje biblioteki" -#: front/src/components/library/radios/Builder.vue:227 -msgid "My awesome radio" -msgstr "Moje wspaniałe radio" - -#: front/src/components/library/Track.vue:64 -#: src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 +#: src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 +#: src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "N/A" #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "Nazwa" @@ -1409,48 +1090,40 @@ msgstr "Nazwa" msgid "New password" msgstr "Nowe hasło" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "Nowe utwory automatycznie pojawią się tutaj." -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "Następny krok" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "Następny utwór" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "Nie" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" msgstr "Brak wtyczek, brak dodatków: potrzebujesz tylko biblioteki sieciowej" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "Tekst nie jest dostępny dla tego utworu." -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" -msgstr "Nikt poza mną" +#: front/src/components/federation/LibraryWidget.vue:6 +#, fuzzy +msgid "No matching library." +msgstr "Ładowanie danych biblioteki…" -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "Nie śledzisz" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" -msgstr "Nie zaimportowano" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" +msgstr "Nikt nie śledzi tej biblioteki" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "Nie użyty" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +msgid "Notifications" +msgstr "Powiadomienia" + +#: front/src/App.vue:36 msgid "Official website" msgstr "Oficjalna strona" @@ -1458,45 +1131,32 @@ msgstr "Oficjalna strona" msgid "Old password" msgstr "Poprzednie hasło" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "Kiedy wszystkie pliki z zostaną wysłane, naciśnij następujący przycisk, aby sprawdzić stan importowania." - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 msgid "Open" msgstr "Otwórz" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "Opcje" -#: front/src/components/library/import/Main.vue:93 -msgid "Or" -msgstr "Lub" - #: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 -#: src/components/library/Radios.vue:33 +#: src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 -#: src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" msgstr "Porządkowanie" #: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 -#: src/components/library/Radios.vue:41 +#: src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 -#: src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "Kolejność porządkowania" @@ -1504,10 +1164,6 @@ msgstr "Kolejność porządkowania" msgid "Owner" msgstr "Właściciel" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "Nie odnaleziono strony" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "Nie odnaleziono strony!" @@ -1516,42 +1172,25 @@ msgstr "Nie odnaleziono strony!" msgid "Password" msgstr "Haslo" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "Zmieniono hasło" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "Pomyślnie zmieniono hasło" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "Wstrzymaj utwór" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "Oczekujące" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" msgstr "Oczekiwanie na przyjęcie" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" -msgstr "Oczekujące prośby o możliwość śledzenia" +#: front/src/views/content/libraries/Quota.vue:22 +msgid "Pending files" +msgstr "Oczekujące pliki" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "Oczekujące prośby o zaimportowanie" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "Oczekujące prośby" @@ -1575,10 +1214,6 @@ msgstr "Odtwórz wszystkie" msgid "Play all albums" msgstr "Odtwórz wszystkie albumy" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "Odtwórz natychmiastowo" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "Odtwórz następny" @@ -1587,14 +1222,6 @@ msgstr "Odtwórz następny" msgid "Play now" msgstr "Odtwórz teraz" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "Odtwórz utwór" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "Lista odtwarzania" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1622,9 +1249,9 @@ msgstr "Zaktualizowano listę odtwarzania" msgid "Playlist visibility" msgstr "Widoczność listy odtwarzania" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" msgstr "Listy odtwarzania" @@ -1644,43 +1271,44 @@ msgstr "Sprawdź dwukrotnie, czy połączenie nazwy użytkownika i hasła jest p msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." msgstr "PNG, GIF lub JPG. Maksymalnie 2MB. Zostanie pomniejszony do 400x400 pikseli." -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "Poprzedni krok" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" -msgstr "Poprzedni utwór" +#: front/src/components/library/FileUpload.vue:58 +msgid "Proceed" +msgstr "Przejdź" #: front/src/views/auth/EmailConfirm.vue:26 #: front/src/views/auth/PasswordResetConfirm.vue:31 msgid "Proceed to login" msgstr "Przejdź, aby zalogować się" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" -msgstr "Data opublikowania" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" +msgstr "Przetwarzanie" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" -msgstr "Szablon zapytania" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" +msgstr "" #: front/src/components/Sidebar.vue:20 msgid "Queue" msgstr "Kolejka" -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "Wymieszano kolejkę!" - -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "Radio" - -#: front/src/components/library/radios/Builder.vue:226 -msgid "Radio Builder" -msgstr "Tworzenie radia" - #: front/src/components/library/radios/Builder.vue:15 msgid "Radio created" msgstr "Utworzono radio" @@ -1694,7 +1322,7 @@ msgid "Radio updated" msgstr "Zaktualizowano radio" #: front/src/components/library/Library.vue:10 -#: src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 msgid "Radios" msgstr "Radia" @@ -1714,19 +1342,11 @@ msgstr "Ostatnio dodane do ulubionych" msgid "Recently listened" msgstr "Ostatnio słuchane" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "ID MusicBrainz nagrania" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" msgstr "Odśwież" -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "Odmówiono" - #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" msgstr "Zarejestrowany od %{ date }" @@ -1739,6 +1359,22 @@ msgstr "Rejestracja na tej instancji jest wyłączona, potrzebujesz kodu zaprasz msgid "regular user" msgstr "zwykły użytkownik" +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "Odrzuć" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "Odrzucono" + +#: front/src/views/content/remote/Home.vue:6 +msgid "Remote libraries" +msgstr "Zdalne biblioteki" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." +msgstr "Zdalne biblioteki należą do innych użytkowników sieci. Możesz uzyskać do nich dostęp jeżeli są publiczne lub został Ci on przyznany." + #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" msgstr "Usuń" @@ -1747,10 +1383,6 @@ msgstr "Usuń" msgid "Remove avatar" msgstr "Usuń awatar" -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "Usuń z ulubionych" - #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" @@ -1764,109 +1396,64 @@ msgstr "Poprosić o nowe hasło API Subsonic?" msgid "Request a password" msgstr "Poproś o nowe hasło" -#: front/src/App.vue:35 -msgid "Request music" -msgstr "Wyślij prośbę o muzykę" - -#: front/src/views/library/MusicRequest.vue:4 -#: src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "Poproś o trochę muzyki" - #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" msgstr "Wysłano prośbę!" -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "Uruchom ponownie zadania zakończone błędem" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "Uruchom ponownie zadanie" - #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" msgstr "Ustaw nowe hasło" -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "Wynik %{ current }/%{ total }" - #: front/src/components/favorites/List.vue:38 #: src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 -#: src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" msgstr "Wyniki na stronę" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:29 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" msgstr "Zapisz" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" -msgstr "Wywołano skanowanie!" +#: front/src/views/content/remote/Card.vue:31 +#, fuzzy +msgid "Scan pending" +msgstr "Rosnąco" + +#: front/src/views/content/remote/Card.vue:43 +#, fuzzy +msgid "Scanned successfully" +msgstr "Pomyślnie zaktualizowano ustawienia." + +#: front/src/views/content/remote/Card.vue:47 +#, fuzzy +msgid "Scanned with errors" +msgstr "Zsynchronizowano zmiany z serwerem" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" +msgstr "" -#: front/src/components/federation/LibraryTrackTable.vue:5 #: front/src/components/library/Artists.vue:10 #: src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:5 #: src/views/playlists/List.vue:13 msgid "Search" msgstr "Szukaj" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "Szukaj zawartości którą chcesz zaimportować:" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "Szukaj według wykonawcy, nazwy użytkownika, komentarzu…" - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "Szukaj według źródła…" - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "Szukaj według wysyłającego, źródła…" - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "Szukaj według tytułu, wykonawcy, domeny…" - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "Szukaj według nazwy użytkownika, domeny…" - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "Szukaj według nazwy użytkownika, adresu e-mail, kodu…" - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "Szukaj według nazwy użytkownika, adresu e-mail, nazwy…" - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." -msgstr "Szukaj wykonawców, albumów, utworów…" +#: front/src/views/content/remote/ScanForm.vue:9 +#, fuzzy +msgid "Search a remote library" +msgstr "Pokaż mi bibliotekę" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" msgstr "Wyszukaj trochę muzyki" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" msgstr "Szukaj na lyrics.wikia.com" @@ -1876,44 +1463,28 @@ msgstr "Szukaj na lyrics.wikia.com" msgid "Search on Wikipedia" msgstr "Szukaj na Wikipedii" -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "Kryterium wyszukiwania" - #: front/src/views/admin/Settings.vue:15 msgid "Sections" msgstr "Sekcje" -#: front/src/components/library/radios/Builder.vue:41 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" msgstr "Zaznacz filtr" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" msgstr[0] "Zaznacz %{ total } element" msgstr[1] "Zaznacz wszystkie %{ total } elementy" msgstr[2] "Zaznacz wszystkie %{ total } elementów" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "Wybierz pliki do wysłania…" - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" msgstr "Zaznacz tylko obecną stronę" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "Wybierz odpowiednie źródła i pliki do zaimportowania" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "Wyślij prośbę o możliwość śledzenia" - -#: front/src/components/Sidebar.vue:97 -#: src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" msgstr "Ustawienia" @@ -1925,14 +1496,28 @@ msgstr "Zaktualizowano ustawienia" msgid "Settings updated successfully." msgstr "Pomyślnie zaktualizowano ustawienia." -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "Ustawienia…" - #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" msgstr "Udostępnij odnośnik" +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." +msgstr "Podziel się tym odnośnikiem z innymi użytkownikami, aby mogli poprosić od dostęp do Twojej biblioteki." + +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +#, fuzzy +msgid "Sharing link" +msgstr "Udostępnij odnośnik" + +#: front/src/components/audio/album/Card.vue:40 +#, fuzzy +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "%{ count } utwór" +msgstr[1] "%{ count } utwory" +msgstr[2] "%{ count } utworów" + #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" @@ -1940,56 +1525,41 @@ msgstr[0] "Pokaż 1 kolejny album" msgstr[1] "Pokaż %{ count } kolejne albumy" msgstr[2] "Pokaż %{ count } kolejnych albumów" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "Pokaż 1 kolejny utwór" -msgstr[1] "Pokaż %{ count } kolejne utwory" -msgstr[2] "Pokaż %{ count } kolejnych utworów" +#: front/src/views/Notifications.vue:10 +#, fuzzy +msgid "Show read notifications" +msgstr "Ostatnia modyfikacja" -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" -msgstr "Pokaż/ukryj hasło" - -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" msgstr "Wyświetlanie wyników %{ start }-%{ end } z %{ total }" -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "Wymieszaj kolejkę" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "Rejestracja" - #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" msgstr "Rejestracja" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "Po prostu skopiuj i wklej poniższy urywek do terminala aby zacząć pobieranie." - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 +#: src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" msgstr "Rozmiar" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 msgid "Skipped" msgstr "Pominięto" +#: front/src/views/content/libraries/Quota.vue:49 +#, fuzzy +msgid "Skipped files" +msgstr "Pominięto" + #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "Brakuje czegoś w bibliotece? Powiedz nam, czego chcesz posłuchać!" @@ -2002,21 +1572,15 @@ msgstr "Przepraszamy, nie znaleziono albumu spełniającego Twoje kryteria" msgid "Sorry, we did not found any artist matching your query" msgstr "Przepraszamy, nie znaleziono wykonawcy spełniającego Twoje kryteria" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "Źródło" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "Kod źródłowy" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "Kod źródłowy (%{version})" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" msgstr "Członek administracji" @@ -2025,26 +1589,11 @@ msgstr "Członek administracji" msgid "Start" msgstr "Rozpocznij" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "Rozpocznij wysyłanie" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "Statystyki" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "Stan" @@ -2052,15 +1601,11 @@ msgstr "Stan" msgid "Stop" msgstr "Zatrzymaj" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" msgstr "Zatrzymaj radio" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "Zatrzymaj wysyłanie" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" msgstr "Wyślij" @@ -2068,46 +1613,43 @@ msgstr "Wyślij" msgid "Submit another request" msgstr "Wyślij kolejną prośbę" -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "Wysłano przez" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "Subsonic" - #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" msgstr "Hasło API Subsonic" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" -msgstr "Powodzenie" - -#: front/src/App.vue:11 +#: front/src/App.vue:13 msgid "Suggested choices" msgstr "Polecane wybory" +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" +msgstr "Podsumowanie" + #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "Synchronizowanie zmian z serwerem…" +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "Skopiowano tekst do schowka!" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." msgstr "To proste: kochamy Grooveshark i chcemy utworzyć coś jeszcze lepszego." -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "The Beatles, Klocuch…" - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." msgstr "Logo Funkwhale zostało zaprojektowane i dostarczone przez Francisa Gadinga." -#: front/src/components/Home.vue:124 +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." +msgstr "Biblioteka i wszystkie utwory z niej zostaną usunięte. To działanie jest nieodwracalne." + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "Pliki muzyczne które wysyłasz są poprawnie otagowane:" + +#: front/src/components/Home.vue:121 msgid "The plaform is free and open-source, you can install it and modify it without worries" msgstr "Platforma jest wolna i otwartoźródłowa, każdy może bez zmartwień zainstalować i modyfikować ją" @@ -2115,33 +1657,37 @@ msgstr "Platforma jest wolna i otwartoźródłowa, każdy może bez zmartwień z msgid "The Subsonic API is not available on this Funkwhale instance." msgstr "API Subsonic nie jest dostępne na tej instancji Funkwhale." -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" -msgstr "The White Album, Aezakmi…" +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" +msgstr "Wysyłane pliki muzyczne są w formacie OGG, FLAC lub MP3" -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." -msgstr "Obecnie nie istnieje sposób na bezpośrednie pobranie wielu utworów z funkwhale jako archiwum ZIP. Możesz jednak użyć narzędzi takich jak cURL, aby łatwo pobrać listę utworów." +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" +msgstr "" -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." -msgstr "To importowanie zostanie przypisane do poniższego żądania. Gdy importowanie zakończy się, żądanie zostanie oznaczone jako wypełnione." +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" +msgstr "" -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" -msgstr "Wskazuje to, czy masz dostęp do zdalnej biblioteki" +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." +msgstr "Ta instancja oferuje maksymalnie %{quota} przestrzeni dla każdego użytkownika." #: front/src/components/auth/Profile.vue:16 msgid "This is you!" msgstr "To Ty!" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." msgstr "Może to wpływać na wiele rzeczy, sprawdź dwukrotnie czy to na pewno to, czego chcesz." -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" -msgstr "Ten utwór nie został zaimportowany i nie może zostać odtworzony" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "" + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" +msgstr "" #: front/src/views/playlists/Detail.vue:37 msgid "This will completely delete this playlist and cannot be undone." @@ -2155,9 +1701,165 @@ msgstr "To bezpowrotnie usunie radio." msgid "This will completely disable access to the Subsonic API using from account." msgstr "To całkowicie wyłączy dostęp do API Subsonic z tego konta." -#: front/src/App.vue:137 +#: front/src/App.vue:162 src/components/About.vue:55 +#: src/components/Home.vue:154 front/src/components/PageNotFound.vue:33 +#: src/components/Sidebar.vue:203 front/src/components/Sidebar.vue:204 +#: src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 +#: src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 +#: src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 +#: src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 +#: src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 +#: src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 +#: src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 +#: src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 +#: src/components/audio/SearchBar.vue:27 front/src/components/auth/Login.vue:77 +#: src/components/auth/Login.vue:78 front/src/components/auth/Logout.vue:20 +#: src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 +#: src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 +#: src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 +#: src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 +#: src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 +#: src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 +#: src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 +#: src/views/auth/EmailConfirm.vue:51 front/src/views/auth/PasswordReset.vue:52 +#: src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +#: src/views/content/Base.vue:19 front/src/views/content/Home.vue:35 +#: src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 +#: src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 +#: src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 +#: src/views/instance/Timeline.vue:57 front/src/views/playlists/Detail.vue:90 +#: src/views/playlists/List.vue:104 front/src/views/playlists/List.vue:105 +#: src/views/radios/Detail.vue:80 +#, fuzzy msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "To wyczyści Twoje lokalne dane i rozłączy Cię, czy chcesz kontynuować?" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "To wyczyści Twoje lokalne dane i rozłączy Cię, czy chcesz kontynuować?" +msgstr[1] "To wyczyści Twoje lokalne dane i rozłączy Cię, czy chcesz kontynuować?" +msgstr[2] "To wyczyści Twoje lokalne dane i rozłączy Cię, czy chcesz kontynuować?" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." @@ -2167,19 +1869,25 @@ msgstr "To wyloguje Cię z urządzeń na których jesteś obecnie zalogowany." msgid "This will remove all tracks from this playlist and cannot be undone." msgstr "To bezpowrotnie usunie wszystkie utwory z tej listy odtwarzania." +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." +msgstr "Ta opcja usunie utwory które zostały wysłane, ale nie udało się ich przetworzyć przez serwer. Bezpowrotnie usunie te pliki i zostanie Ci przywrócona odpowiednia przestrzeń." + +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "Ta opcja usunie utwory które zostały wysłane, ale nie zostały jeszcze przetworzone. Bezpowrotnie usunie te pliki i zostanie Ci przywrócona odpowiednia przestrzeń." + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." +msgstr "Ta opcja usunie utwory które zostały wysłane, lecz zostały z jakiegoś powodu pominięte w procesie importowania. Bezpowrotnie usunie te pliki i zostanie Ci przywrócona odpowiednia przestrzeń." + #: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 #: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 msgid "Title" msgstr "Tytuł" -#: front/src/components/audio/SearchBar.vue:27 -#: src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" -msgstr "Utwór" - #: front/src/components/library/Track.vue:53 msgid "Track information" msgstr "Informacje o utworze" @@ -2194,15 +1902,11 @@ msgstr "utwory" #: front/src/components/library/Album.vue:43 #: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 -#: front/src/views/radios/Detail.vue:34 +#: src/views/content/Base.vue:8 front/src/views/content/libraries/Detail.vue:8 +#: src/views/playlists/Detail.vue:50 front/src/views/radios/Detail.vue:34 msgid "Tracks" msgstr "Utwory" -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "Utwory dostępne w bibliotece" - #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" msgstr "Utwory tego wykonawcy" @@ -2215,34 +1919,39 @@ msgstr "Ulubione utwory" msgid "tracks listened" msgstr "wysłuchane utwory" -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "Wywołaj skanowanie" - +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" msgstr "Rodzaj" +#: front/src/views/content/remote/Card.vue:100 +#: src/views/content/remote/Card.vue:105 +#, fuzzy +msgid "Unfollow" +msgstr "Śledź" + +#: front/src/views/content/remote/Card.vue:101 +#, fuzzy +msgid "Unfollow this library?" +msgstr "Czy chcesz usunąć tę bibliotekę?" + #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." msgstr "Niestety, właściciele tej instancji nie znaleźli czasu na wypełnienie tej strony." -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "Nieznany" - #: front/src/components/Home.vue:37 msgid "Unlimited music" msgstr "Nieograniczona muzyka" -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "Cofnij wyciszenie" - #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" msgstr "Aktualizuj awatar" +#: front/src/views/content/libraries/Form.vue:25 +#, fuzzy +msgid "Update library" +msgstr "Zarządzaj biblioteką" + #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" msgstr "Aktualizuj listę odtwarzania" @@ -2255,8 +1964,10 @@ msgstr "Aktualizuj ustawienia" msgid "Update your password" msgstr "Aktualizuj swoje hasło" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" msgstr "Wyślij" @@ -2264,39 +1975,57 @@ msgstr "Wyślij" msgid "Upload a new avatar" msgstr "Dodaj nowy awatar" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" -msgstr "Wysłane pliki lub z zewnętrznego źródła" +#: front/src/views/content/Home.vue:6 +#, fuzzy +msgid "Upload audio content" +msgstr "Dodaj nowy awatar" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/views/content/libraries/FilesTable.vue:54 +#, fuzzy +msgid "Upload date" +msgstr "Wyślij" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "Wyślij pliki muzyczne (mp3, ogg, flac itp.) ze swojej biblioteki bezpośrednio z przeglądarki, aby cieszyć się nimi tutaj." + +#: front/src/components/library/FileUpload.vue:31 +#, fuzzy +msgid "Upload new tracks" +msgstr "Dodaj nowy awatar" + +#: front/src/views/admin/users/UsersDetail.vue:82 +#, fuzzy +msgid "Upload quota" +msgstr "Wyślij" + +#: front/src/components/library/FileUpload.vue:99 +#, fuzzy +msgid "Uploaded" +msgstr "Wyślij" + +#: front/src/components/library/FileUpload.vue:5 +#, fuzzy +msgid "Uploading" +msgstr "Wysyłanie…" + +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "Wysyłanie…" -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" msgstr "Skorzystaj z innej instancji" -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "Użyj tego pola komentarzy, aby dodać szczegóły zgłoszenia, jeżeli potrzebne" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "Użyj tej opcji aby wyłączyć/włączyć federację dla tej biblioteki" - #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." msgstr "Użyj tego formularza aby poprosić o zresetowanie hasła. Otrzymasz e-mail z instrukcjami resetowania hasła na podany adres." -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "Użyj tego formularza aby przeskanować instancję i skonfigurować federację." - #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" msgstr "Zużyty" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" msgstr "Użytkownik" @@ -2304,12 +2033,20 @@ msgstr "Użytkownik" msgid "User activity" msgstr "Aktywność użytkownika" +#: front/src/components/library/Album.vue:49 +#: src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +#, fuzzy +msgid "User libraries" +msgstr "Moje biblioteki" + #: front/src/components/library/Radios.vue:20 msgid "User radios" msgstr "Radia użytkownika" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" msgstr "Nazwa użytkownika" @@ -2321,21 +2058,32 @@ msgstr "Nazwa użytkownika lub adres e-mail" msgid "users" msgstr "użytkownicy" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 +#: src/views/admin/users/UsersList.vue:21 front/src/views/admin/Settings.vue:81 msgid "Users" msgstr "Użytkownicy" +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +#, fuzzy +msgid "View files" +msgstr "Pliki z biblioteki" + #: front/src/components/library/Album.vue:37 #: src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" msgstr "Wyświetl na MusicBrainz" +#: front/src/views/content/libraries/Form.vue:18 +#, fuzzy +msgid "Visibility" +msgstr "Widoczność listy odtwarzania" + #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" msgstr "Nie udało się dodać tego utworu do listy odtwarzania" @@ -2360,12 +2108,21 @@ msgstr "Nie udało się zapisać awatara" msgid "We cannot save your settings" msgstr "Nie udało się zapisać ustawień" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" msgstr "Nie śledzimy Cię i nie wyświetlamy Ci reklam" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "Brak powiadomień do wyświetlenia!" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." +msgstr "" + +#: front/src/components/library/FileUpload.vue:40 +#, fuzzy +msgid "we recommend using Picard for that purpose" msgstr "Polecamy używać do tego Picard." #: front/src/components/Home.vue:7 @@ -2380,27 +2137,15 @@ msgstr "Przepraszamy, strona której szukasz nie istnieje:" msgid "We've received your request, you'll get some groove soon ;)" msgstr "Otrzymaliśmy Twoje zgłoszenie, wkrótce dostaniesz trochę groove-u ;)" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "Witamy" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" msgstr "Witamy na Funkwhale" -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "Czym są metadane?" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "Jeżeli włączone, automatyczne importowanie będzie automatycznie importować nowe utwory opublikowane w tej bibliotece" - #: front/src/components/Home.vue:24 msgid "Why funkwhale?" msgstr "Dlaczego funkwhale?" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" msgstr "Tak" @@ -2408,15 +2153,19 @@ msgstr "Tak" msgid "Yes, log me out!" msgstr "Tak, wyloguj mnie!" +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" +msgstr "" + #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" msgstr "Jesteś obecnie zalogowany jako %{ username }" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." -msgstr "Możesz też pominąć ten krok i wprowadzić metadane ręcznie." +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." +msgstr "" -#: front/src/components/Home.vue:136 +#: front/src/components/Home.vue:133 msgid "You can invite friends and family to your instance so they can enjoy your music" msgstr "Możesz zaprosić znajomych i rodzinę na swoją instancję, aby mogli się cieszyć dodaną przez Ciebie muzyką" @@ -2428,14 +2177,18 @@ msgstr "Możesz używać tego interfejsu aby utworzyć własne radio, które bę msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." msgstr "Możesz używać tego, aby cieszyć się muzyką i swoją listą odtwarzania w trybie offline, na przykład na smartfonie i tablecie." -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" msgstr "Odtwarzasz radio" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" msgstr "Musisz wybrać instancję aby kontynuować" +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." +msgstr "" + #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" msgstr "Zostaniesz wylogowany z tej sesji i musisz zalogować się nowym hasłem" @@ -2444,22 +2197,19 @@ msgstr "Zostaniesz wylogowany z tej sesji i musisz zalogować się nowym hasłem msgid "You will have to update your password on your clients that use this password." msgstr "Będziesz musiał zmienić hasło na klientach używających tego hasła." -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "Zaimportujesz:" - #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." msgstr "Twój adres e-mail został potwierdzony, możesz używać usługi bez ograniczeń." -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" -msgstr "Twoje ulubione" - -#: front/src/components/Home.vue:117 +#: front/src/components/Home.vue:114 msgid "Your music, your way" msgstr "Twoja muzyka, po Twojemu" +#: front/src/views/Notifications.vue:7 +#, fuzzy +msgid "Your notifications" +msgstr "Ostatnia modyfikacja" + #: front/src/views/auth/PasswordResetConfirm.vue:29 msgid "Your password has been updated successfully." msgstr "Twoje hasło zostało pomyślnie zmienione." @@ -2468,9 +2218,426 @@ msgstr "Twoje hasło zostało pomyślnie zmienione." msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" msgstr "Twoje hasło Subsonic zostanie zmienione na nowe, losowe i zostaniesz wylogowany z urządzeń korzystających ze starego hasła Subsonic" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +#, fuzzy +msgid "Activity visibility" +msgstr "Widoczność listy odtwarzania" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 +#: src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "Nikt poza mną" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 +#: src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "Wszyscy na tej instancji" + +#: front/src/components/mixins/Translations.vue:18 +#, fuzzy +msgid "Accessed date" +msgstr "Brak dostępu" + +#: front/src/components/mixins/Translations.vue:19 +#, fuzzy +msgid "Modification date" +msgstr "Data wygaśnięcia" + +#: front/src/components/mixins/Translations.vue:20 +#, fuzzy +msgid "Imported date" +msgstr "Data zaimportowania" + +#: front/src/components/mixins/Translations.vue:22 +#, fuzzy +msgid "Track name" +msgstr "Utwór" + +#: front/src/components/mixins/Translations.vue:23 +#, fuzzy +msgid "Album name" +msgstr "Strona albumu" + +#: front/src/components/mixins/Translations.vue:30 +#, fuzzy +msgid "Sign-up date" +msgstr "Rejestracja" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "Skopiuj utwory z obecnej kolejki do listy odtwarzania" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "Dodaj do tej playlisty" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "Moja wspaniała playlista" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "Wszyscy" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "Rejestracja" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "Wprowadź swój kod zapraszający (wielkość znaków nie ma znaczenia)" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "Wprowadź swoją nazwę użytkownika" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "Wprowadź swój e-mail" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "Zmieniono hasło" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "Brak dostępu" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "Wprowadź swoją nazwę użytkownika lub e-mail" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "Zaloguj się" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "Profil %{ username }" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "Wyloguj się" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "Ustawienia konta" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "Usuń z ulubionych" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "Twoje ulubione" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "Wprowadź nazwę radia…" + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "Tworzenie radia" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "Moje wspaniałe radio" + +#: front/src/components/library/radios/Builder.vue:236 +#, fuzzy +msgid "My awesome description" +msgstr "Moje wspaniałe radio" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "" + +#: front/src/components/library/FileUpload.vue:240 +#, fuzzy +msgid "A network error occured while uploading this file" +msgstr "Wystąpił błąd podczas zapisywania zmian" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "Wprowadź nazwę wykonawcy…" + +#: front/src/components/library/Track.vue:195 +#: src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "Utwór" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "Strona główna" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "Pokaż/ukryj hasło" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "The Beatles, Klocuch…" + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "The White Album, Aezakmi…" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "Użyj tego pola komentarzy, aby dodać szczegóły zgłoszenia, jeżeli potrzebne" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" msgstr[0] "Dodano %{ count } utwór do kolejki" msgstr[1] "Dodano %{ count } utwory do kolejki" msgstr[2] "Dodano %{ count } utworów do kolejki" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "Wykonawca, album, utwór…" + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "Szukaj wykonawców, albumów, utworów…" + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "Wymieszano kolejkę!" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "Poprzedni utwór" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "Odtwórz utwór" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "Wstrzymaj utwór" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "Następny utwór" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "Cofnij wyciszenie" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "Wycisz" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "Zapętlanie jest wyłączone. Naciśnij, aby przełączyć na powtarzanie jednego utworu." + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "Zapętlanie jednego utworu jest włączone. Naciśnij, aby przełączyć na powtarzanie całej kolejki." + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "Zapętlanie całej kolejki jest włączone. Naciśnij, aby wyłączyć zapętlanie." + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "Wymieszaj kolejkę" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "Wyczyść swoją kolejkę" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "Oczekujące prośby o zaimportowanie" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "Oczekujące prośby o możliwość śledzenia" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "Wprowadź swoje kryterium wyszukiwania…" + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "Szukaj według tytułu, wykonawcy, domeny…" + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "Pozostaw puste, aby wygenerować kod" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "Szukaj według nazwy użytkownika, adresu e-mail, kodu…" + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "Szukaj według nazwy użytkownika, adresu e-mail, nazwy…" + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "Federacja" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "" + +#: front/src/views/content/remote/ScanForm.vue:48 +#, fuzzy +msgid "Enter a library url" +msgstr "Wprowadź nazwę domeny biblioteki…" + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:226 +#, fuzzy +msgid "Search by title, artist, album..." +msgstr "Szukaj według tytułu, wykonawcy, domeny…" + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "" + +#: front/src/views/content/libraries/FilesTable.vue:242 +#, fuzzy +msgid "Import went on successfully" +msgstr "Pomyślnie zmieniono hasło" + +#: front/src/views/content/libraries/FilesTable.vue:259 +#, fuzzy +msgid "Relaunch import" +msgstr "Zakończ importowanie" + +#: front/src/views/content/libraries/Card.vue:58 +#, fuzzy +msgid "Visibility: nobody except me" +msgstr "Nikt poza mną" + +#: front/src/views/content/libraries/Card.vue:59 +#, fuzzy +msgid "Visibility: everyone on this instance" +msgstr "Wszyscy na tej instancji" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "" + +#: front/src/views/content/libraries/Card.vue:61 +#, fuzzy +msgid "Total size of the files in this library" +msgstr "Utwory dostępne w bibliotece" + +#: front/src/views/content/libraries/Form.vue:70 +#, fuzzy +msgid "My awesome library" +msgstr "Moje wspaniałe radio" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "" + +#: front/src/views/content/libraries/Form.vue:74 +#, fuzzy +msgid "Everyone, including other instances" +msgstr "Wszyscy na tej instancji" + +#: front/src/views/content/libraries/Form.vue:106 +#, fuzzy +msgid "Library updated" +msgstr "Nazwa biblioteki" + +#: front/src/views/content/libraries/Form.vue:109 +#, fuzzy +msgid "Library created" +msgstr "Nazwa biblioteki" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "Radio" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "Oś czasu instancji" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "Lista odtwarzania" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "Wprowadź nazwę listy odtwarzania…" + +#: front/src/views/admin/library/Base.vue:16 +#, fuzzy +msgid "Manage library" +msgstr "W bibliotece" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "Określa, czy konto użytkownika jest aktywne. Nieaktywni użytkownicy nie mogą zalogować się i korzystać z usługi." + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "" + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "Zarządzaj użytkownikami" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "Ustawienia instancji" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "Informacje o instancji" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "Subsonic" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "Statystyki" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "Zgłaszanie błędów" diff --git a/front/locales/pt_PT/LC_MESSAGES/app.po b/front/locales/pt_PT/LC_MESSAGES/app.po index 03361742c..62c9ce17f 100644 --- a/front/locales/pt_PT/LC_MESSAGES/app.po +++ b/front/locales/pt_PT/LC_MESSAGES/app.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: front 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-17 19:29+0200\n" -"PO-Revision-Date: 2018-07-22 20:29+0000\n" +"POT-Creation-Date: 2018-10-05 20:10+0200\n" +"PO-Revision-Date: 2018-10-07 09:06+0000\n" "Last-Translator: troll \n" "Language-Team: none\n" "Language: pt_PT\n" @@ -30,14 +30,16 @@ msgstr "" msgid "(empty)" msgstr "(vazio)" -#: front/src/components/common/ActionTable.vue:43 -#: front/src/components/common/ActionTable.vue:51 +#: front/src/components/common/ActionTable.vue:44 +#: front/src/components/common/ActionTable.vue:53 msgid "%{ count } on %{ total } selected" msgid_plural "%{ count } on %{ total } selected" msgstr[0] "" msgstr[1] "" -#: front/src/components/Sidebar.vue:116 src/views/federation/LibraryDetail.vue:87 +#: front/src/components/Sidebar.vue:107 src/components/audio/album/Card.vue:54 +#: front/src/views/content/libraries/Card.vue:39 +#: src/views/content/remote/Card.vue:26 msgid "%{ count } track" msgid_plural "%{ count } tracks" msgstr[0] "%{ count } tema" @@ -49,7 +51,7 @@ msgid_plural "%{ count } tracks in %{ albumsCount } albums" msgstr[0] "%{ count } tema em %{ albumsCount } álbuns" msgstr[1] "%{ count } temas em %{ albumsCount } álbuns" -#: front/src/components/library/radios/Builder.vue:66 +#: front/src/components/library/radios/Builder.vue:80 msgid "%{ count } track matching combined filters" msgid_plural "%{ count } tracks matching combined filters" msgstr[0] "%{ count } música corresponde aos filtros selecionados" @@ -61,13 +63,15 @@ msgid_plural "%{ count } tracks" msgstr[0] "%{ count} música" msgstr[1] "%{ count } músicas" +#: front/src/views/content/libraries/Quota.vue:11 +msgid "%{ current } used on %{ max } allowed" +msgstr "" + #: front/src/components/common/Duration.vue:2 -#, fuzzy msgid "%{ hours } h %{ minutes } min" msgstr "%{ hours } h %{ minutes } min" #: front/src/components/common/Duration.vue:5 -#, fuzzy msgid "%{ minutes } min" msgstr "%{ minutes } min" @@ -79,10 +83,6 @@ msgstr "%{ user } gostou de uma música" msgid "%{ user } listened to a track" msgstr "%{ user } escutou uma música" -#: front/src/components/auth/Profile.vue:49 -msgid "%{ username }'s profile" -msgstr "%{ username } perfil" - #: front/src/components/audio/artist/Card.vue:41 msgid "1 album" msgid_plural "%{ count } albums" @@ -95,39 +95,29 @@ msgid_plural "%{ count } favorites" msgstr[0] "1 preferida" msgstr[1] "%{ count } preferidas" -#: front/src/components/audio/album/Card.vue:54 -#: front/src/components/federation/LibraryCard.vue:25 -msgid "1 track" -msgid_plural "%{ count } tracks" -msgstr[0] "1 música" -msgstr[1] "%{ count } músicas" - #: front/src/components/About.vue:5 msgid "About %{ instance }" msgstr "Sobre %{ instance }" -#: front/src/App.vue:54 +#: front/src/App.vue:53 msgid "About Funkwhale" msgstr "Sobre Funkwhale" -#: front/src/App.vue:32 src/components/About.vue:8 src/components/About.vue:55 +#: front/src/App.vue:34 src/components/About.vue:8 src/components/About.vue:55 msgid "About this instance" msgstr "Sobre esta instância" -#: front/src/components/manage/library/RequestsTable.vue:28 -#: front/src/components/manage/library/RequestsTable.vue:62 +#: front/src/views/content/libraries/Detail.vue:48 +msgid "Accept" +msgstr "Aceitar" + +#: front/src/views/content/libraries/Detail.vue:40 msgid "Accepted" msgstr "Aceitado" -#: front/src/components/auth/SubsonicTokenForm.vue:111 -msgid "Access disabled" -msgstr "Acesso desativado" - -#: front/src/components/Home.vue:109 +#: front/src/components/Home.vue:106 msgid "Access your music from a clean interface that focus on what really matters" -msgstr "" -"Acesse sua música a partir de uma interface limpa que se concentra no que " -"realmente importa" +msgstr "Acesse sua música a partir de uma interface limpa que se concentra no que realmente importa" #: front/src/views/admin/users/UsersDetail.vue:54 msgid "Account active" @@ -137,10 +127,6 @@ msgstr "Conta ativa" msgid "Account settings" msgstr "Configurações da conta" -#: front/src/components/auth/Settings.vue:257 -msgid "Account Settings" -msgstr "Configurações da conta" - #: front/src/components/manage/users/UsersTable.vue:39 msgid "Account status" msgstr "Status da Conta" @@ -149,16 +135,18 @@ msgstr "Status da Conta" msgid "Account's email" msgstr "Email da conta" -#: front/src/components/common/ActionTable.vue:82 +#: front/src/views/content/libraries/Detail.vue:29 +msgid "Action" +msgstr "Açao" + +#: front/src/components/common/ActionTable.vue:86 msgid "Action %{ action } was launched successfully on %{ count } element" msgid_plural "Action %{ action } was launched successfully on %{ count } elements" msgstr[0] "A ação %{ action } foi lançada com sucesso em %{ count } item" msgstr[1] "A ação %{ action } foi lançada com sucesso em %{ count } itens" #: front/src/components/common/ActionTable.vue:8 -#: front/src/components/federation/LibraryFollowTable.vue:24 -#: front/src/components/library/radios/Builder.vue:50 -#: front/src/components/manage/library/RequestsTable.vue:54 +#: front/src/components/library/radios/Builder.vue:64 msgid "Actions" msgstr "Ações" @@ -166,30 +154,22 @@ msgstr "Ações" msgid "Active" msgstr "Ativo" -#: front/src/components/Sidebar.vue:60 +#: front/src/components/Sidebar.vue:75 msgid "Activity" msgstr "Atividade" -#: front/src/components/federation/LibraryFollowTable.vue:21 -msgid "Actor" -msgstr "Ator" +#: front/src/components/Sidebar.vue:78 src/views/content/Base.vue:19 +msgid "Add content" +msgstr "Adicionar conteúdo" -#: front/src/views/federation/LibraryList.vue:8 -msgid "Add a new library" -msgstr "Adicione uma nova biblioteca" - -#: front/src/components/library/radios/Builder.vue:36 +#: front/src/components/library/radios/Builder.vue:50 msgid "Add filter" msgstr "Adicionar um filtro" -#: front/src/components/library/radios/Builder.vue:26 +#: front/src/components/library/radios/Builder.vue:40 msgid "Add filters to customize your radio" msgstr "Adicione filtros para personalizar seu rádio" -#: front/src/components/audio/PlayButton.vue:53 -msgid "Add to current queue" -msgstr "Adicionar à fila atual" - #: front/src/components/favorites/TrackFavoriteIcon.vue:4 #: front/src/components/favorites/TrackFavoriteIcon.vue:21 msgid "Add to favorites" @@ -204,10 +184,6 @@ msgstr "Adicionar à Playlist..." msgid "Add to queue" msgstr "Adicionar à fila" -#: front/src/components/playlists/PlaylistModal.vue:115 -msgid "Add to this playlist" -msgstr "Adicionar a esta playlist" - #: front/src/components/playlists/PlaylistModal.vue:54 msgid "Add track" msgstr "Adicionar música" @@ -216,24 +192,19 @@ msgstr "Adicionar música" msgid "Admin" msgstr "Admin" -#: front/src/components/Sidebar.vue:64 +#: front/src/components/Sidebar.vue:82 msgid "Administration" msgstr "Administração" -#: front/src/components/audio/SearchBar.vue:26 src/components/audio/track/Table.vue:8 -#: front/src/components/federation/LibraryTrackTable.vue:49 -#: front/src/components/library/Album.vue:91 +#: front/src/components/audio/track/Table.vue:8 #: front/src/components/manage/library/FilesTable.vue:39 +#: front/src/views/content/libraries/FilesTable.vue:53 +#: front/src/components/library/Album.vue:101 +#: src/components/audio/SearchBar.vue:26 #: front/src/components/metadata/Search.vue:134 msgid "Album" msgstr "Álbum" -#: front/src/components/library/import/ReleaseImport.vue:3 -msgid "Album %{ title } (%{ count } track) by %{ artist }" -msgid_plural "Album %{ title } (%{ count } tracks) by %{ artist }" -msgstr[0] "" -msgstr[1] "" - #: front/src/components/library/Album.vue:12 msgid "Album containing %{ count } track, by %{ artist }" msgid_plural "Album containing %{ count } tracks, by %{ artist }" @@ -244,8 +215,8 @@ msgstr[1] "" msgid "Album page" msgstr "Página do álbum" -#: front/src/components/audio/Search.vue:19 src/components/instance/Stats.vue:48 -#: front/src/components/manage/library/RequestsTable.vue:50 +#: front/src/components/audio/Search.vue:19 +#: src/components/instance/Stats.vue:48 #: front/src/components/requests/Form.vue:9 msgid "Albums" msgstr "Álbuns" @@ -254,8 +225,8 @@ msgstr "Álbuns" msgid "Albums by this artist" msgstr "Álbuns deste artista" -#: front/src/components/manage/library/RequestsTable.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:19 +#: front/src/views/content/libraries/FilesTable.vue:13 msgid "All" msgstr "Tudo" @@ -265,82 +236,52 @@ msgstr "Ocorreu um erro ao salvar suas mudanças" #: front/src/components/auth/Login.vue:10 msgid "An unknown error happend, this can mean the server is down or cannot be reached" -msgstr "" -"Um erro desconhecido aconteceu, isso pode significar que o servidor está " -"inoperante ou não pode ser alcançado" - -#: front/src/components/federation/LibraryTrackTable.vue:11 -#: front/src/components/library/import/BatchDetail.vue:68 -#: front/src/components/library/import/BatchList.vue:13 -#: front/src/components/library/import/BatchList.vue:22 -msgid "Any" -msgstr "Qualquer" - -#: front/src/components/library/import/BatchList.vue:24 -msgid "API" -msgstr "API" - -#: front/src/components/federation/LibraryFollowTable.vue:68 -#: front/src/components/federation/LibraryFollowTable.vue:78 -msgid "Approve" -msgstr "Aprovar" - -#: front/src/components/federation/LibraryFollowTable.vue:70 -msgid "Approve access?" -msgstr "Aprovar acesso?" - -#: front/src/components/federation/LibraryFollowTable.vue:38 -msgid "Approved" -msgstr "Aprovado" +msgstr "Um erro desconhecido aconteceu, isso pode significar que o servidor está inoperante ou não pode ser alcançado" #: front/src/components/auth/Logout.vue:5 msgid "Are you sure you want to log out?" msgstr "Tem certeza que deseja sair?" -#: front/src/components/audio/SearchBar.vue:25 src/components/audio/track/Table.vue:7 -#: front/src/components/federation/LibraryTrackTable.vue:48 -#: front/src/components/library/Artist.vue:119 +#: front/src/components/audio/track/Table.vue:7 #: front/src/components/manage/library/FilesTable.vue:38 -#: front/src/components/manage/library/RequestsTable.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:52 +#: front/src/components/library/Artist.vue:129 +#: src/components/audio/SearchBar.vue:25 #: front/src/components/metadata/Search.vue:130 msgid "Artist" msgstr "Artista" #: front/src/components/requests/Form.vue:5 +#: src/components/mixins/Translations.vue:24 msgid "Artist name" msgstr "Nome do artista" -#: front/src/components/library/Album.vue:22 src/components/library/Track.vue:23 +#: front/src/components/library/Album.vue:22 +#: src/components/library/Track.vue:23 msgid "Artist page" msgstr "Página do artista" -#: front/src/components/audio/Search.vue:65 -msgid "Artist, album, track..." -msgstr "Artista, álbum, música ..." - -#: front/src/components/audio/Search.vue:10 src/components/instance/Stats.vue:42 -#: front/src/components/library/Artists.vue:119 src/components/library/Library.vue:7 +#: front/src/components/audio/Search.vue:10 +#: src/components/instance/Stats.vue:42 +#: front/src/components/library/Library.vue:7 +#: src/components/library/Artists.vue:120 msgid "Artists" msgstr "Artistas" #: front/src/components/favorites/List.vue:33 -#: front/src/components/federation/LibraryTrackTable.vue:28 -#: front/src/components/library/Artists.vue:25 src/components/library/Radios.vue:44 +#: src/components/library/Artists.vue:25 +#: front/src/components/library/Radios.vue:44 #: front/src/components/manage/library/FilesTable.vue:19 -#: front/src/components/manage/library/RequestsTable.vue:19 #: front/src/components/manage/users/UsersTable.vue:19 -#: front/src/views/federation/LibraryList.vue:28 src/views/playlists/List.vue:27 +#: front/src/views/content/libraries/FilesTable.vue:31 +#: front/src/views/playlists/List.vue:27 msgid "Ascending" -msgstr "" +msgstr "Ascendente" #: front/src/views/auth/PasswordReset.vue:27 msgid "Ask for a password reset" msgstr "Peça uma redefinição de senha" -#: front/src/views/federation/LibraryDetail.vue:56 -msgid "Auto importing" -msgstr "Importação automática" - #: front/src/components/playlists/PlaylistModal.vue:26 msgid "Available playlists" msgstr "Playlists disponíveis" @@ -354,14 +295,9 @@ msgstr "Avatar" msgid "Back to login" msgstr "Volte ao login" -#: front/src/views/federation/LibraryFollowersList.vue:5 -msgid "Be careful when accepting follow requests, as it means the follower will have access to your entire library." -msgstr "" -"Tenha cuidado ao aceitar as solicitações a seguir, pois isso significa que o " -"seguidor terá acesso a toda a sua biblioteca." - #: front/src/components/library/Track.vue:80 #: front/src/components/manage/library/FilesTable.vue:42 +#: front/src/components/mixins/Translations.vue:28 msgid "Bitrate" msgstr "" @@ -369,7 +305,7 @@ msgstr "" msgid "Browse" msgstr "Procurar" -#: front/src/components/Sidebar.vue:50 +#: front/src/components/Sidebar.vue:65 msgid "Browse library" msgstr "Navegar pela biblioteca" @@ -377,18 +313,6 @@ msgstr "Navegar pela biblioteca" msgid "Browsing artists" msgstr "Procurando artistas" -#: front/src/views/federation/LibraryTrackList.vue:3 -msgid "Browsing federated tracks" -msgstr "Procurando músicas federadas" - -#: front/src/views/federation/LibraryFollowersList.vue:3 -msgid "Browsing followers" -msgstr "Navegando seguidores" - -#: front/src/views/federation/LibraryList.vue:3 -msgid "Browsing libraries" -msgstr "Navegue por bibliotecas" - #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" msgstr "Procure playlists" @@ -405,22 +329,18 @@ msgstr "Construtor" msgid "By %{ artist }" msgstr "Por %{ artist }" -#: front/src/components/federation/LibraryFollowTable.vue:57 -msgid "By confirming, %{ username } will be denied access to your library." -msgstr "Ao confirmar, %{ username } não terá acesso à sua biblioteca." +#: front/src/views/content/remote/Card.vue:103 +msgid "By unfollowing this library, you will loose access to its content." +msgstr "" +"Ao deixar de seguir esta biblioteca, você perderá o acesso ao seu conteúdo." -#: front/src/components/federation/LibraryFollowTable.vue:73 -msgid "By confirming, %{ username } will be granted access to your library." -msgstr "Ao confirmar, %{ username } terá acesso à sua biblioteca." - -#: front/src/components/audio/track/Table.vue:43 #: front/src/components/common/DangerousButton.vue:17 #: front/src/components/library/radios/Filter.vue:53 #: front/src/components/playlists/PlaylistModal.vue:63 msgid "Cancel" msgstr "Cancelar" -#: front/src/components/library/radios/Builder.vue:49 +#: front/src/components/library/radios/Builder.vue:63 msgid "Candidates" msgstr "Candidatos" @@ -428,7 +348,7 @@ msgstr "Candidatos" msgid "Cannot change your password" msgstr "Não é possível alterar sua senha" -#: front/src/App.vue:66 +#: front/src/App.vue:65 msgid "Change language" msgstr "Mudar idioma" @@ -455,15 +375,13 @@ msgstr "Modificações sincronizadas com o servidor" #: front/src/components/auth/Settings.vue:70 msgid "Changing your password will also change your Subsonic API password if you have requested one." -msgstr "" -"Mudar sua senha também muda sua senha da API Subsonic se você tiver " -"solicitado uma." +msgstr "Mudar sua senha também muda sua senha da API Subsonic se você tiver solicitado uma." #: front/src/components/auth/Settings.vue:98 msgid "Changing your password will have the following consequences" msgstr "Alterar sua senha terá as seguintes consequências" -#: front/src/App.vue:4 +#: front/src/App.vue:6 msgid "Choose your instance" msgstr "Escolha sua instância" @@ -480,22 +398,15 @@ msgstr "Claro" msgid "Clear playlist" msgstr "Limpar playlist" -#: front/src/components/audio/Player.vue:270 -msgid "Clear your queue" -msgstr "Limpar sua fila" - -#: front/src/components/library/import/BatchList.vue:23 -msgid "CLI" -msgstr "CLI" - #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" msgstr "Clique uma vez, ouça por horas usando rádios" -#: front/src/components/manage/library/RequestsTable.vue:30 -#: front/src/components/manage/library/RequestsTable.vue:64 -msgid "Closed" -msgstr "Fechado" +#: front/src/components/library/FileUpload.vue:76 +msgid "Click to select files to upload or drag and drop files or directories" +msgstr "" +"Clique para selecionar arquivos para carregar ou arrastar e soltar arquivos " +"ou diretórios" #: front/src/components/manage/users/InvitationForm.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:42 @@ -508,12 +419,11 @@ msgstr "Código" msgid "Collapse" msgstr "Colapso" -#: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" msgstr "Comentário" -#: front/src/components/library/radios/Builder.vue:48 +#: front/src/components/library/radios/Builder.vue:62 msgid "Config" msgstr "Configuração" @@ -530,11 +440,11 @@ msgstr "Confirme seu email" msgid "Confirmation code" msgstr "Código de confirmação" -#: front/src/components/playlists/Editor.vue:163 -msgid "Copy tracks from current queue to playlist" -msgstr "Copiar músicas da fila atual para a playlist" +#: front/src/components/common/CopyInput.vue:8 +msgid "Copy" +msgstr "Cópia" -#: front/src/components/Home.vue:88 +#: front/src/components/Home.vue:85 msgid "Covers, lyrics, our goal is to have them all ;)" msgstr "Capas, letras, nosso objetivo é tê-los todos ;)" @@ -542,19 +452,26 @@ msgstr "Capas, letras, nosso objetivo é tê-los todos ;)" msgid "Create a funkwhale account" msgstr "Crie uma conta funkwhale" +#: front/src/views/content/libraries/Home.vue:14 +msgid "Create a new library" +msgstr "Criar uma nova biblioteca" + #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" msgstr "Crie uma nova playlist" -#: front/src/components/auth/Login.vue:17 +#: front/src/components/Sidebar.vue:57 src/components/auth/Login.vue:17 msgid "Create an account" msgstr "Crie a sua conta" -#: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" msgstr "Criar importação" +#: front/src/views/content/libraries/Form.vue:26 +msgid "Create library" +msgstr "Criar biblioteca" + #: front/src/components/auth/Signup.vue:51 msgid "Create my account" msgstr "Criar a minha conta" @@ -567,9 +484,8 @@ msgstr "Crie uma playlist" msgid "Create your own radio" msgstr "Crie seu próprio rádio" -#: front/src/components/federation/LibraryFollowTable.vue:22 -#: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 +#: front/src/components/mixins/Translations.vue:17 msgid "Creation date" msgstr "Data de criação" @@ -577,17 +493,34 @@ msgstr "Data de criação" msgid "Current avatar" msgstr "Avatar atual" +#: front/src/views/content/libraries/DetailArea.vue:4 +msgid "Current library" +msgstr "Biblioteca atual" + #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" msgstr "Música atual" -#: front/src/components/manage/library/FilesTable.vue:189 -#: front/src/components/manage/library/RequestsTable.vue:195 -#: front/src/components/manage/users/InvitationsTable.vue:166 -#: front/src/views/playlists/Detail.vue:33 +#: front/src/views/content/libraries/Quota.vue:2 +msgid "Current usage" +msgstr "Uso atual" + +#: front/src/views/content/libraries/Detail.vue:27 +msgid "Date" +msgstr "Data" + +#: front/src/views/content/libraries/Form.vue:29 +#: src/views/playlists/Detail.vue:33 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/views/content/libraries/FilesTable.vue:258 msgid "Delete" msgstr "Suprimir" +#: front/src/views/content/libraries/Form.vue:39 +msgid "Delete library" +msgstr "Excluir biblioteca" + #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" msgstr "Suprimir Playlist" @@ -596,34 +529,32 @@ msgstr "Suprimir Playlist" msgid "Delete radio" msgstr "Suprimir radio" -#: front/src/components/federation/LibraryFollowTable.vue:52 -#: front/src/components/federation/LibraryFollowTable.vue:63 -msgid "Deny" -msgstr "Negar" - -#: front/src/components/federation/LibraryFollowTable.vue:54 -msgid "Deny access?" -msgstr "Negar acesso?" +#: front/src/views/content/libraries/Form.vue:31 +msgid "Delete this library?" +msgstr "Excluir esta biblioteca?" #: front/src/components/favorites/List.vue:34 -#: front/src/components/federation/LibraryTrackTable.vue:29 -#: front/src/components/library/Artists.vue:26 src/components/library/Radios.vue:47 +#: src/components/library/Artists.vue:26 +#: front/src/components/library/Radios.vue:47 #: front/src/components/manage/library/FilesTable.vue:20 -#: front/src/components/manage/library/RequestsTable.vue:20 #: front/src/components/manage/users/UsersTable.vue:20 -#: front/src/views/federation/LibraryList.vue:29 src/views/playlists/List.vue:28 +#: front/src/views/content/libraries/FilesTable.vue:32 +#: front/src/views/playlists/List.vue:28 msgid "Descending" msgstr "Descendente" -#: front/src/components/federation/LibraryCard.vue:50 +#: front/src/components/library/radios/Builder.vue:25 +#: front/src/views/content/libraries/Form.vue:14 +msgid "Description" +msgstr "Descrição" + +#: front/src/views/content/libraries/Card.vue:47 msgid "Detail" msgstr "Detalhe" -#: front/src/views/admin/users/UsersDetail.vue:146 -msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." -msgstr "" -"Determine se a conta do usuário está ativa ou não. Usuários inativos não " -"podem fazer autenticação ou usar o serviço." +#: front/src/views/content/remote/Card.vue:50 +msgid "Details" +msgstr "Detalhes" #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 @@ -642,7 +573,7 @@ msgstr "Desativar o acesso o API Subsonic?" msgid "Discover how to use Funkwhale from other apps" msgstr "Descubra como usar o Funkwhale em outros aplicativos" -#: front/src/components/library/radios/Builder.vue:16 +#: front/src/components/library/radios/Builder.vue:30 msgid "Display publicly" msgstr "Exibir publicamente" @@ -659,8 +590,8 @@ msgid "Do you want to delete the playlist \"%{ playlist }\"?" msgstr "Você deseja excluir a playlist \"%{ playlist }\"?" #: front/src/views/radios/Detail.vue:26 -msgid "Do you want to delete the radio \"{{ radio }}\"?" -msgstr "Você deseja excluir a radio \"{{ radio }}\"?" +msgid "Do you want to delete the radio \"%{ radio }\"?" +msgstr "Você deseja excluir a radio \"%{ radio }\"?" #: front/src/components/common/ActionTable.vue:29 msgid "Do you want to launch %{ action } on %{ count } element?" @@ -668,40 +599,43 @@ msgid_plural "Do you want to launch %{ action } on %{ count } elements?" msgstr[0] "Deseja lançar %{ action } no %{ count } elemento?" msgstr[1] "Deseja lançar %{ action } nos %{ count } elementos?" -#: front/src/components/Sidebar.vue:113 +#: front/src/components/Sidebar.vue:104 msgid "Do you want to restore your previous queue?" msgstr "Você quer restaurar sua fila anterior?" -#: front/src/App.vue:38 +#: front/src/App.vue:37 msgid "Documentation" msgstr "Documentação" -#: front/src/components/audio/track/Table.vue:24 src/components/library/Track.vue:48 +#: front/src/components/library/Track.vue:48 msgid "Download" msgstr "Descarregar" -#: front/src/components/audio/track/Table.vue:27 -msgid "Download tracks" -msgstr "Descarregar músicas" - #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" msgstr "Arraste e solte as linhas para reordenar as músicas da playlist" -#: front/src/components/library/Track.vue:58 +#: front/src/components/audio/track/Table.vue:9 +#: src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 +#: front/src/views/content/libraries/FilesTable.vue:56 +#: front/src/components/mixins/Translations.vue:29 msgid "Duration" msgstr "Duração" -#: front/src/components/Home.vue:96 +#: front/src/components/Home.vue:93 msgid "Easy to use" msgstr "Fácil de usar" +#: front/src/views/content/libraries/Detail.vue:9 +msgid "Edit" +msgstr "Editar" + #: front/src/components/About.vue:21 msgid "Edit instance info" msgstr "Editar informações da instância" -#: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 +#: front/src/components/radios/Card.vue:22 src/views/playlists/Detail.vue:30 msgid "Edit..." msgstr "Editar..." @@ -722,57 +656,16 @@ msgstr "E-mail confirmado" msgid "End edition" msgstr "" -#: front/src/components/library/import/FileUpload.vue:4 -msgid "Ensure your music files are properly tagged before uploading them." -msgstr "" -"Certifique-se de que seus arquivos de música estejam marcados corretamente " -"antes de carregá-los." - -#: front/src/components/library/Radios.vue:140 -msgid "Enter a radio name..." -msgstr "Indicar um nome de rádio ..." - -#: front/src/components/library/Artists.vue:118 -msgid "Enter an artist name..." -msgstr "Indicar um nome de artista ..." - -#: front/src/views/federation/LibraryList.vue:122 -msgid "Enter an library domain name..." -msgstr "Indicar um nome de domínio de biblioteca ..." - -#: front/src/views/playlists/List.vue:104 -msgid "Enter an playlist name..." -msgstr "Indicar um nome de playlist..." - -#: front/src/components/auth/Signup.vue:98 -msgid "Enter your email" -msgstr "Insira seu email" - -#: front/src/components/auth/Signup.vue:96 -msgid "Enter your invitation code (case insensitive)" -msgstr "Indicar seu código de convite (não diferencia maiúsculas de minúsculas)" - -#: front/src/components/metadata/Search.vue:114 -msgid "Enter your search query..." -msgstr "Insira sua consulta de pesquisa ..." - -#: front/src/components/auth/Signup.vue:97 -msgid "Enter your username" -msgstr "Indicar seu nome de usuário" - -#: front/src/components/auth/Login.vue:77 -msgid "Enter your username or email" -msgstr "Indicar seu nome de usuário ou email" - #: front/src/components/auth/SubsonicTokenForm.vue:20 +#: front/src/views/content/libraries/Form.vue:4 msgid "Error" msgstr "Erro" -#: front/src/views/admin/Settings.vue:87 -msgid "Error reporting" -msgstr "Relatório de erros" +#: front/src/views/content/remote/Card.vue:39 +msgid "Error during scan" +msgstr "Erro durante a análise" -#: front/src/components/common/ActionTable.vue:75 +#: front/src/components/common/ActionTable.vue:79 msgid "Error while applying action" msgstr "Erro ao aplicar a ação" @@ -792,29 +685,28 @@ msgstr "Erro ao confirmar seu email" msgid "Error while creating invitation" msgstr "Erro ao criar convite" +#: front/src/views/content/remote/ScanForm.vue:3 +msgid "Error while fetching remote library" +msgstr "Erro ao recuperar a biblioteca remota" + #: front/src/components/admin/SettingsGroup.vue:5 msgid "Error while saving settings" msgstr "Erro ao salvar configurações" -#: front/src/components/federation/LibraryForm.vue:10 -msgid "Error while scanning library" -msgstr "Erro ao verificar biblioteca" - -#: front/src/components/library/import/BatchDetail.vue:41 -#: front/src/components/library/import/BatchDetail.vue:70 -#: front/src/components/library/import/BatchList.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:16 +#: front/src/views/content/libraries/FilesTable.vue:237 msgid "Errored" msgstr "Errado" -#: front/src/components/playlists/Form.vue:89 -msgid "Everyone" -msgstr "Todo o mundo" +#: front/src/views/content/libraries/Quota.vue:75 +msgid "Errored files" +msgstr "Arquivos errados" -#: front/src/components/playlists/Form.vue:85 -msgid "Everyone on this instance" -msgstr "Todos nessa instância" +#: front/src/views/content/remote/Card.vue:58 +msgid "Errored tracks:" +msgstr "Músicas com erros:" -#: front/src/components/library/radios/Builder.vue:47 +#: front/src/components/library/radios/Builder.vue:61 msgid "Exclude" msgstr "Excluir" @@ -823,6 +715,7 @@ msgid "Expand" msgstr "Expandir" #: front/src/components/manage/users/InvitationsTable.vue:41 +#: front/src/components/mixins/Translations.vue:21 msgid "Expiration date" msgstr "Data de vencimento" @@ -834,92 +727,51 @@ msgstr "Expirado" msgid "Expired/used" msgstr "Expirado / usados" -#: front/src/components/library/import/Main.vue:65 -msgid "External source. Supported backends" -msgstr "" - -#: front/src/components/Sidebar.vue:51 +#: front/src/components/Sidebar.vue:66 msgid "Favorites" msgstr "Favoritas" -#: front/src/components/federation/LibraryForm.vue:3 -msgid "Federate with a new instance" -msgstr "Federar com uma nova instância" - -#: front/src/views/federation/LibraryTrackList.vue:21 -msgid "Federated tracks" -msgstr "Músicas federadas" - -#: front/src/components/Sidebar.vue:87 src/components/library/import/BatchList.vue:25 -#: front/src/components/manage/users/UsersTable.vue:180 -#: front/src/views/admin/Settings.vue:84 src/views/admin/users/UsersDetail.vue:161 -#: front/src/views/federation/Base.vue:35 src/views/federation/LibraryDetail.vue:40 -msgid "Federation" -msgstr "Federação" - -#: front/src/views/federation/LibraryDetail.vue:3 -msgid "File mirroring" -msgstr "" - -#: front/src/components/library/import/FileUpload.vue:43 +#: front/src/components/library/FileUpload.vue:83 msgid "File name" msgstr "Nome do ficheiro" -#: front/src/components/library/import/Main.vue:76 -msgid "File upload" -msgstr "Enviar ficheiros" - -#: front/src/views/admin/library/Base.vue:5 src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/library/Base.vue:5 +#: src/views/admin/library/FilesList.vue:21 msgid "Files" msgstr "Ficheiros" -#: front/src/components/library/import/ArtistImport.vue:7 -msgid "Filter album types" -msgstr "Filtrar tipos de álbuns" - -#: front/src/components/library/radios/Builder.vue:46 +#: front/src/components/library/radios/Builder.vue:60 msgid "Filter name" msgstr "Nome do filtro" -#: front/src/components/library/import/Main.vue:52 -msgid "Finish import" -msgstr "Acabar a importação" - -#: front/src/components/library/import/BatchDetail.vue:54 +#: front/src/views/content/libraries/FilesTable.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:241 msgid "Finished" msgstr "Acabado" -#: front/src/components/library/import/Main.vue:59 -msgid "First, choose where you want to import the music from" -msgstr "Primeiro, escolha onde você quer importar a música de" - -#: front/src/components/federation/LibraryCard.vue:44 +#: front/src/views/content/remote/Card.vue:83 msgid "Follow" msgstr "Segue" -#: front/src/components/federation/LibraryCard.vue:36 -msgid "Follow request pending approval" -msgstr "Aprovar a solicitação de seguir pendente" +#: front/src/views/content/remote/Card.vue:88 +#, fuzzy +msgid "Follow pending approval" +msgstr "Solicitação de seguir pendente" -#: front/src/views/federation/LibraryDetail.vue:21 -msgid "Follow status" -msgstr "Siga o status" +#: front/src/views/content/Home.vue:16 +msgid "Follow remote libraries" +msgstr "Siga as bibliotecas remotas" -#: front/src/views/federation/Base.vue:13 -#: front/src/views/federation/LibraryFollowersList.vue:24 +#: front/src/views/content/libraries/Detail.vue:7 msgid "Followers" msgstr "Seguidores" -#: front/src/components/federation/LibraryCard.vue:18 -msgid "Followers only" -msgstr "Só seguidores" - -#: front/src/components/federation/LibraryCard.vue:15 -#: front/src/views/federation/LibraryDetail.vue:29 +#: front/src/views/content/remote/Card.vue:93 msgid "Following" msgstr "Seguir" -#: front/src/components/activity/Like.vue:12 src/components/activity/Listen.vue:12 +#: front/src/components/activity/Like.vue:14 +#: src/components/activity/Listen.vue:14 msgid "from %{ album } by %{ artist }" msgstr "de %{ album } por %{ artist }" @@ -927,30 +779,23 @@ msgstr "de %{ album } por %{ artist }" msgid "From album %{ album } by %{ artist }" msgstr "Do álbum %{ album } por %{ artist }" -#: front/src/App.vue:56 +#: front/src/App.vue:55 msgid "Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!" -msgstr "" -"O Funkwhale é um projeto gratuito e de código aberto executado por " -"voluntários. Você pode nos ajudar a melhorar a plataforma reportando bugs, " -"sugerindo recursos e compartilhando o projeto com seus amigos!" +msgstr "O Funkwhale é um projeto gratuito e de código aberto executado por voluntários. Você pode nos ajudar a melhorar a plataforma reportando bugs, sugerindo recursos e compartilhando o projeto com seus amigos!" #: front/src/components/auth/SubsonicTokenForm.vue:7 msgid "Funkwhale is compatible with other music players that support the Subsonic API." -msgstr "" -"O Funkwhale é compatível com outros players de música que suportam a API " -"Subsonic." +msgstr "O Funkwhale é compatível com outros players de música que suportam a API Subsonic." -#: front/src/components/Home.vue:98 +#: front/src/components/Home.vue:95 msgid "Funkwhale is dead simple to use." msgstr "Funkwhale é muito simples de usar." #: front/src/components/Home.vue:39 msgid "Funkwhale is designed to make it easy to listen to music you like, or to discover new artists." -msgstr "" -"O Funkwhale foi projetado para facilitar a escuta da música que você gosta " -"ou descobrir novos artistas." +msgstr "O Funkwhale foi projetado para facilitar a escuta da música que você gosta ou descobrir novos artistas." -#: front/src/components/Home.vue:119 +#: front/src/components/Home.vue:116 msgid "Funkwhale is free and gives you control on your music." msgstr "O Funkwhale é gratuito e permite controlar sua música." @@ -966,17 +811,15 @@ msgstr "Receba um novo convite" msgid "Get me to the library" msgstr "Me leve para a biblioteca" -#: front/src/components/Home.vue:77 -msgid "" -"Get quality metadata about your music thanks to\n" -" \n" -" MusicBrainz\n" -" " +#: front/src/components/Home.vue:76 +msgid "Get quality metadata about your music thanks to MusicBrainz" msgstr "" -"Obtenha metadados de qualidade sobre sua música\n" -" \n" -" MusicBrainz\n" -" " +"Obtenha metadados de qualidade sobre sua música graças ao MusicBrainz" + +#: front/src/views/content/Home.vue:12 src/views/content/Home.vue:19 +msgid "Get started" +msgstr "Começar" #: front/src/components/common/ActionTable.vue:21 #: front/src/components/common/ActionTable.vue:27 @@ -987,13 +830,9 @@ msgstr "Vá" msgid "Go to home page" msgstr "Vá para a página inicial" -#: front/src/components/library/import/Main.vue:13 -msgid "Grab corresponding metadata" -msgstr "Pegue os metadados correspondentes" - -#: front/src/components/library/Home.vue:65 -msgid "Home" -msgstr "Casa" +#: front/src/App.vue:73 +msgid "Help us translate Funkwhale" +msgstr "Ajude-nos a traduzir o Funkwhale" #: front/src/components/instance/Stats.vue:36 msgid "Hours of music" @@ -1001,157 +840,47 @@ msgstr "Horas de música" #: front/src/components/auth/SubsonicTokenForm.vue:11 msgid "However, accessing Funkwhale from those clients require a separate password you can set below." -msgstr "" -"No entanto, acessar o Funkwhale desses clientes requer uma senha separada " -"que você pode definir abaixo." - -#: front/src/components/library/import/BatchList.vue:34 -msgid "ID" -msgstr "ID" +msgstr "No entanto, acessar o Funkwhale desses clientes requer uma senha separada que você pode definir abaixo." #: front/src/views/auth/PasswordResetConfirm.vue:24 msgid "If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes." -msgstr "" -"Se o endereço de e-mail fornecido na etapa anterior for válido e vinculado a " -"uma conta de usuário, você deverá receber um e-mail com instruções de " -"redefinição nos próximos minutos." - -#: front/src/components/federation/LibraryTrackTable.vue:196 -#: front/src/components/library/Library.vue:17 -msgid "Import" -msgstr "Importação" - -#: front/src/components/federation/LibraryTrackTable.vue:57 -msgid "Import #%{ id } launched" -msgstr "Importação#%{ id } lançado" - -#: front/src/components/library/import/Main.vue:38 -msgid "Import %{ count } track" -msgid_plural "Import %{ count } tracks" -msgstr[0] "Importar %{ count } música" -msgstr[1] "Importar %{ count } músicas" - -#: front/src/components/library/import/BatchDetail.vue:10 -msgid "Import batch" -msgstr "Importa lote" - -#: front/src/components/library/import/BatchDetail.vue:185 -msgid "Import Batch #%{ id }" -msgstr "Importa lote #%{ id }" - -#: front/src/components/library/Library.vue:20 -msgid "Import batches" -msgstr "Importação de lotes" - -#: front/src/components/library/import/BatchList.vue:117 -msgid "Import Batches" -msgstr "Importação de lotes" +msgstr "Se o endereço de e-mail fornecido na etapa anterior for válido e vinculado a uma conta de usuário, você deverá receber um e-mail com instruções de redefinição nos próximos minutos." #: front/src/components/manage/library/FilesTable.vue:40 -#: front/src/components/manage/library/RequestsTable.vue:53 msgid "Import date" msgstr "Data de importação" -#: front/src/components/library/import/FileUpload.vue:38 -msgid "Import detail page" -msgstr "" - -#: front/src/components/Sidebar.vue:81 -msgid "Import music" -msgstr "Importar música" - -#: front/src/components/library/import/Main.vue:267 -msgid "Import Music" -msgstr "Importar Música" - #: front/src/components/Home.vue:71 msgid "Import music from various platforms, such as YouTube or SoundCloud" msgstr "Importe músicas de várias plataformas, como o YouTube ou o SoundCloud" -#: front/src/components/federation/LibraryTrackTable.vue:14 -#: front/src/components/federation/LibraryTrackTable.vue:66 -msgid "Import pending" -msgstr "Importação pendente" +#: front/src/components/library/FileUpload.vue:51 +msgid "Import reference" +msgstr "Referência de importação" -#: front/src/views/admin/library/Base.vue:9 -#: front/src/views/admin/library/RequestsList.vue:3 -#: front/src/views/admin/library/RequestsList.vue:21 -msgid "Import requests" -msgstr "Solicitações de importação" - -#: front/src/components/library/import/BatchList.vue:20 -#: front/src/components/library/import/Main.vue:6 -msgid "Import source" -msgstr "Origem de importação" - -#: front/src/components/federation/LibraryTrackTable.vue:9 +#: front/src/views/content/libraries/FilesTable.vue:11 +#: front/src/views/content/libraries/FilesTable.vue:55 msgid "Import status" msgstr "Status de Importação" -#: front/src/components/library/import/ReleaseImport.vue:14 -msgid "Import this release" -msgstr "Importar esta publicação" - -#: front/src/components/library/import/TrackImport.vue:11 -msgid "Import this track" -msgstr "Importar esta música" - -#: front/src/components/federation/LibraryTrackTable.vue:12 -#: front/src/components/manage/library/RequestsTable.vue:29 -#: front/src/components/manage/library/RequestsTable.vue:61 -msgid "Imported" -msgstr "Importado" - -#: front/src/components/library/import/TrackImport.vue:44 -msgid "Imported URL" -msgstr "URL importado" - -#: front/src/views/admin/Settings.vue:82 -msgid "Imports" -msgstr "Importações" - #: front/src/components/favorites/TrackFavoriteIcon.vue:3 msgid "In favorites" msgstr "Nos favoritos" -#: front/src/components/federation/LibraryTrackTable.vue:65 -msgid "In library" -msgstr "Na biblioteca" - #: front/src/components/manage/users/UsersTable.vue:54 msgid "Inactive" msgstr "Inativo" -#: front/src/components/library/import/Main.vue:96 -msgid "Input a MusicBrainz ID manually:" -msgstr "Insira um ID do MusicBrainz manualmente:" - -#: front/src/views/auth/PasswordReset.vue:53 -msgid "Input the email address binded to your account" -msgstr "Insira o endereço de e-mail vinculado à sua conta" - #: front/src/components/playlists/Editor.vue:31 msgid "Insert from queue (%{ count } track)" msgid_plural "Insert from queue (%{ count } tracks)" msgstr[0] "Inserir da fila (%{ count } música)" msgstr[1] "Inserir da fila (%{ count } músicas)" -#: front/src/views/admin/Settings.vue:80 -msgid "Instance information" -msgstr "Informação da instância" - #: front/src/components/library/Radios.vue:9 msgid "Instance radios" msgstr "Rádios da instância" -#: front/src/views/admin/Settings.vue:75 -msgid "Instance settings" -msgstr "Configurações da instância" - -#: front/src/views/instance/Timeline.vue:57 -msgid "Instance Timeline" -msgstr "Linha do tempo da instância" - #: front/src/components/auth/Signup.vue:42 #: front/src/components/manage/users/InvitationForm.vue:11 msgid "Invitation code" @@ -1161,79 +890,72 @@ msgstr "Código de Convite" msgid "Invitation code (optional)" msgstr "Código de convite (opcional)" -#: front/src/views/admin/users/Base.vue:8 src/views/admin/users/InvitationsList.vue:3 +#: front/src/views/admin/users/Base.vue:8 +#: src/views/admin/users/InvitationsList.vue:3 #: front/src/views/admin/users/InvitationsList.vue:24 msgid "Invitations" msgstr "Convites" -#: front/src/App.vue:43 +#: front/src/App.vue:42 msgid "Issue tracker" msgstr "Rastreador de problemas" -#: front/src/components/library/import/BatchDetail.vue:80 -msgid "Job ID" -msgstr "" - -#: front/src/components/library/import/BatchList.vue:36 -msgid "Jobs" -msgstr "" +#: front/src/views/content/libraries/Home.vue:9 +msgid "It looks like you don't have any library yet, it's time to create one!" +msgstr "Parece que você ainda não tem biblioteca, é hora de criar uma!" #: front/src/components/Home.vue:50 msgid "Keep a track of your favorite songs" -msgstr "" +msgstr "Guardar suas músicas favoritas" -#: front/src/components/audio/track/Table.vue:33 -msgid "Keep your PRIVATE_TOKEN secret as it gives access to your account." -msgstr "Mantenha o seu PRIVATE_TOKEN segredo, pois dá acesso à sua conta." +#: front/src/views/content/remote/Home.vue:14 +msgid "Known libraries" +msgstr "Bibliotecas conhecidas" #: front/src/components/manage/users/UsersTable.vue:41 #: front/src/views/admin/users/UsersDetail.vue:45 +#: front/src/components/mixins/Translations.vue:31 msgid "Last activity" msgstr "Ultima atividade" -#: front/src/views/federation/LibraryDetail.vue:101 -msgid "Last fetched" -msgstr "Última recuperação" - #: front/src/components/playlists/PlaylistModal.vue:32 msgid "Last modification" msgstr "Última modificação" -#: front/src/components/common/ActionTable.vue:39 +#: front/src/views/content/remote/Card.vue:56 +msgid "Last update:" +msgstr "Última atualização:" + +#: front/src/components/common/ActionTable.vue:40 msgid "Launch" msgstr "Lançamento" -#: front/src/components/library/import/BatchDetail.vue:18 -#: front/src/components/library/import/BatchList.vue:35 -msgid "Launch date" -msgstr "Data de lançamento" - -#: front/src/components/federation/LibraryForm.vue:31 +#: front/src/views/content/remote/Card.vue:63 msgid "Launch scan" -msgstr "" +msgstr "Iniciar um scan" #: front/src/components/Home.vue:10 msgid "Learn more about this instance" msgstr "Saiba mais sobre esta instância" -#: front/src/components/manage/users/InvitationForm.vue:58 -msgid "Leave empty for a random code" -msgstr "Deixar vazio para um código aleatório" - #: front/src/components/requests/Form.vue:10 msgid "Leave this field empty if you're requesting the whole discography." msgstr "Deixe essa área vazia se você estiver solicitando toda a discografia." -#: front/src/views/federation/Base.vue:5 src/views/federation/LibraryList.vue:123 +#: front/src/views/content/Base.vue:5 msgid "Libraries" msgstr "Bibliotecas" -#: front/src/components/Sidebar.vue:70 -#: front/src/components/federation/LibraryTrackTable.vue:51 +#: front/src/views/content/libraries/Form.vue:2 +msgid "Libraries help you organize and share your music collections. You can upload your own music collection to Funkwhale and share it with your friends and family." +msgstr "" +"As bibliotecas ajudam você a organizar e compartilhar suas coleções de " +"músicas. Você pode enviar sua própria coleção de músicas para o Funkwhale e " +"compartilhá-la com seus amigos e familiares." + #: front/src/components/instance/Stats.vue:30 -#: front/src/components/manage/users/UsersTable.vue:176 -#: front/src/views/admin/users/UsersDetail.vue:157 -#: front/src/views/federation/LibraryDetail.vue:194 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 msgid "Library" msgstr "Biblioteca" @@ -1241,155 +963,111 @@ msgstr "Biblioteca" msgid "Library files" msgstr "Ficheiros da biblioteca" -#: front/src/components/federation/LibraryForm.vue:20 -msgid "Library name" -msgstr "Nome da biblioteca" - -#: front/src/views/federation/LibraryDetail.vue:84 -msgid "Library size" -msgstr "Tamanho da biblioteca" - -#: front/src/components/federation/LibraryForm.vue:96 -msgid "library@demo.funkwhale.audio" -msgstr "library@demo.funkwhale.audio" - -#: front/src/App.vue:29 +#: front/src/App.vue:31 msgid "Links" msgstr "Ligações" +#: front/src/views/content/libraries/Detail.vue:21 +msgid "Loading followers..." +msgstr "Carregando seguidores..." + +#: front/src/views/content/libraries/Home.vue:3 +msgid "Loading Libraries..." +msgstr "Carregando bibliotecas..." + +#: front/src/views/content/libraries/Detail.vue:3 +#: front/src/views/content/libraries/Upload.vue:3 +msgid "Loading library data..." +msgstr "Carregando dados da biblioteca..." + +#: front/src/views/Notifications.vue:4 +msgid "Loading notifications..." +msgstr "Carregando notificações..." + +#: front/src/views/content/remote/Home.vue:3 +msgid "Loading remote libraries..." +msgstr "Carregando bibliotecas remotas..." + #: front/src/views/instance/Timeline.vue:4 msgid "Loading timeline..." msgstr "Carregando a linha do tempo ..." +#: front/src/views/content/libraries/Quota.vue:4 +msgid "Loading usage data..." +msgstr "Carregando dados de uso..." + #: front/src/components/favorites/List.vue:5 msgid "Loading your favorites..." msgstr "Carregando seus favoritos ..." -#: front/src/components/auth/Login.vue:78 -msgid "Log In" -msgstr "Autenticação" - #: front/src/components/auth/Login.vue:4 msgid "Log in to your Funkwhale account" msgstr "Logar na sua conta Funkwhale" -#: front/src/components/auth/Logout.vue:20 -msgid "Log Out" -msgstr "Sair" - #: front/src/components/Sidebar.vue:38 msgid "Logged in as %{ username }" msgstr "Conectado como %{ username }" -#: front/src/components/Sidebar.vue:44 src/components/auth/Login.vue:41 +#: front/src/components/Sidebar.vue:54 src/components/auth/Login.vue:41 msgid "Login" msgstr "Entrar" -#: front/src/components/Sidebar.vue:43 +#: front/src/components/Sidebar.vue:52 msgid "Logout" msgstr "Sair" -#: front/src/components/audio/Player.vue:266 -msgid "Looping disabled. Click to switch to single-track looping." -msgstr "Looping desativado. Clique para alternar para um loop de musica única." - -#: front/src/components/audio/Player.vue:267 -msgid "Looping on a single track. Click to switch to whole queue looping." -msgstr "" -"Looping em uma única música. Clique para alternar para todo o loop da fila." - -#: front/src/components/audio/Player.vue:268 -msgid "Looping on whole queue. Click to disable looping." -msgstr "Looping na fila inteira. Clique para desativar o loop." - -#: front/src/components/library/Track.vue:94 +#: front/src/components/library/Track.vue:105 msgid "Lyrics" msgstr "Letras" -#: front/src/views/admin/library/Base.vue:25 -msgid "Manage library" -msgstr "Gerenciar biblioteca" - #: front/src/components/playlists/PlaylistModal.vue:3 msgid "Manage playlists" msgstr "Gerenciar playlists" -#: front/src/views/admin/users/Base.vue:20 -msgid "Manage users" -msgstr "Gerenciar usuários" - #: front/src/views/playlists/List.vue:8 msgid "Manage your playlists" msgstr "Gerenciar suas playlists" -#: front/src/components/manage/library/RequestsTable.vue:197 -msgid "Mark as closed" -msgstr "Marcar como fechado" +#: front/src/views/Notifications.vue:17 +msgid "Mark all as read" +msgstr "Marque tudo como lido" -#: front/src/components/manage/library/RequestsTable.vue:196 -msgid "Mark as imported" -msgstr "Marcar como importado" - -#: front/src/components/library/import/Main.vue:12 -msgid "Metadata" -msgstr "Metadados" - -#: front/src/components/library/import/Main.vue:115 -msgid "" -"Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the\n" -" \n" -" MusicBrainz\n" -" \n" -" project, which you can think about as the Wikipedia of music." +#: front/src/views/admin/users/UsersDetail.vue:94 +msgid "MB" msgstr "" -"Metadados são os dados relacionados à música que você deseja importar. Isso " -"inclui todas as informações sobre os artistas, álbuns e músicas. Para ter " -"uma biblioteca de alta qualidade, recomenda-se obter dados do\n" -" \n" -" MusicBrainz\n" -" \n" -" projeto, que você pode considerar como a Wikipedia da música." -#: front/src/components/Sidebar.vue:48 src/components/library/import/Main.vue:18 +#: front/src/components/Sidebar.vue:63 msgid "Music" msgstr "Música" -#: front/src/components/library/import/Main.vue:147 -msgid "Music request" -msgstr "Pedido de música" - -#: front/src/components/audio/Player.vue:265 -msgid "Mute" -msgstr "Mudo" - #: front/src/components/Sidebar.vue:34 msgid "My account" msgstr "Minha conta" -#: front/src/components/playlists/Form.vue:74 -msgid "My awesome playlist" -msgstr "Minha playlist incrível" +#: front/src/views/content/libraries/Home.vue:6 +msgid "My libraries" +msgstr "Minhas bibliotecas" -#: front/src/components/library/radios/Builder.vue:206 -msgid "My awesome radio" -msgstr "Meu rádio incrível" - -#: front/src/components/library/Track.vue:64 src/components/library/Track.vue:75 -#: front/src/components/library/Track.vue:86 +#: front/src/components/audio/track/Row.vue:40 +#: src/components/library/Track.vue:64 +#: front/src/components/library/Track.vue:75 +#: src/components/library/Track.vue:86 +#: front/src/components/library/Track.vue:97 #: front/src/components/manage/library/FilesTable.vue:63 #: front/src/components/manage/library/FilesTable.vue:69 #: front/src/components/manage/library/FilesTable.vue:75 #: front/src/components/manage/library/FilesTable.vue:81 -#: front/src/components/manage/library/RequestsTable.vue:71 -#: front/src/components/manage/library/RequestsTable.vue:75 -#: front/src/components/manage/library/RequestsTable.vue:82 #: front/src/components/manage/users/UsersTable.vue:61 #: front/src/views/admin/users/UsersDetail.vue:49 +#: front/src/views/content/libraries/FilesTable.vue:89 +#: front/src/views/content/libraries/FilesTable.vue:95 msgid "N/A" msgstr "" #: front/src/components/playlists/PlaylistModal.vue:31 #: front/src/views/admin/users/UsersDetail.vue:21 +#: front/src/views/content/libraries/Form.vue:10 +#: front/src/components/mixins/Translations.vue:25 msgid "Name" msgstr "Nome" @@ -1398,48 +1076,39 @@ msgstr "Nome" msgid "New password" msgstr "Nova senha" -#: front/src/components/Sidebar.vue:158 +#: front/src/components/Sidebar.vue:149 msgid "New tracks will be appended here automatically." msgstr "Novas músicas serão adicionadas automaticamente aqui." -#: front/src/components/library/import/Main.vue:29 -msgid "Next step" -msgstr "Próxima etapa" - -#: front/src/components/audio/Player.vue:263 -msgid "Next track" -msgstr "Música seguinte" - -#: front/src/components/Sidebar.vue:125 +#: front/src/components/Sidebar.vue:116 msgid "No" msgstr "Não" -#: front/src/components/Home.vue:103 +#: front/src/components/Home.vue:100 msgid "No add-ons, no plugins : you only need a web library" msgstr "Sem add-ons, sem plugins: você só precisa de uma biblioteca da Web" -#: front/src/components/library/Track.vue:102 +#: front/src/components/library/Track.vue:113 msgid "No lyrics available for this track." msgstr "Nenhuma letra disponível para esta música." -#: front/src/components/playlists/Form.vue:81 -msgid "Nobody except me" -msgstr "Ninguém, exceto eu" +#: front/src/components/federation/LibraryWidget.vue:6 +msgid "No matching library." +msgstr "Nenhuma biblioteca correspondente." -#: front/src/views/federation/LibraryDetail.vue:32 -msgid "Not following" -msgstr "Não seguir" - -#: front/src/components/federation/LibraryTrackTable.vue:13 -#: front/src/components/federation/LibraryTrackTable.vue:67 -msgid "Not imported" -msgstr "Não importado" +#: front/src/views/content/libraries/Detail.vue:57 +msgid "Nobody is following this library" +msgstr "Ninguém está seguindo esta biblioteca" #: front/src/components/manage/users/InvitationsTable.vue:51 msgid "Not used" msgstr "Não usado" -#: front/src/App.vue:37 +#: front/src/components/Sidebar.vue:46 src/views/Notifications.vue:74 +msgid "Notifications" +msgstr "Notificações" + +#: front/src/App.vue:36 msgid "Official website" msgstr "Website oficial" @@ -1447,45 +1116,32 @@ msgstr "Website oficial" msgid "Old password" msgstr "Senha Antiga" -#: front/src/components/library/import/FileUpload.vue:36 -msgid "Once all your files are uploaded, simply click the following button to check the import status." -msgstr "" -"Depois que todos os seus ficheiros foram carregados, basta clicar no botão a " -"seguir para verificar o status de importação." - -#: front/src/components/federation/LibraryCard.vue:21 #: front/src/components/manage/users/InvitationsTable.vue:20 -#, fuzzy msgid "Open" msgstr "Aberto" -#: front/src/App.vue:63 +#: front/src/App.vue:62 msgid "Options" msgstr "Opções" -#: front/src/components/library/import/Main.vue:93 -#, fuzzy -msgid "Or" -msgstr "Ou" - #: front/src/components/favorites/List.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:18 -#: front/src/components/library/Artists.vue:15 src/components/library/Radios.vue:33 +#: src/components/library/Artists.vue:15 +#: front/src/components/library/Radios.vue:33 #: front/src/components/manage/library/FilesTable.vue:9 -#: front/src/components/manage/library/RequestsTable.vue:9 #: front/src/components/manage/users/InvitationsTable.vue:9 #: front/src/components/manage/users/UsersTable.vue:9 -#: front/src/views/federation/LibraryList.vue:18 src/views/playlists/List.vue:17 +#: front/src/views/content/libraries/FilesTable.vue:21 +#: front/src/views/playlists/List.vue:17 msgid "Ordering" msgstr "Ordenar" #: front/src/components/favorites/List.vue:31 -#: front/src/components/federation/LibraryTrackTable.vue:26 -#: front/src/components/library/Artists.vue:23 src/components/library/Radios.vue:41 +#: src/components/library/Artists.vue:23 +#: front/src/components/library/Radios.vue:41 #: front/src/components/manage/library/FilesTable.vue:17 -#: front/src/components/manage/library/RequestsTable.vue:17 #: front/src/components/manage/users/UsersTable.vue:17 -#: front/src/views/federation/LibraryList.vue:26 src/views/playlists/List.vue:25 +#: front/src/views/content/libraries/FilesTable.vue:29 +#: front/src/views/playlists/List.vue:25 msgid "Ordering direction" msgstr "Direção de ordenar" @@ -1493,10 +1149,6 @@ msgstr "Direção de ordenar" msgid "Owner" msgstr "Proprietário" -#: front/src/components/PageNotFound.vue:33 -msgid "Page Not Found" -msgstr "Página não encontrada" - #: front/src/components/PageNotFound.vue:7 msgid "Page not found!" msgstr "Página não encontrada!" @@ -1505,42 +1157,25 @@ msgstr "Página não encontrada!" msgid "Password" msgstr "Palavra-passe" -#: front/src/components/auth/SubsonicTokenForm.vue:95 -msgid "Password updated" -msgstr "Senha atualizada" - #: front/src/views/auth/PasswordResetConfirm.vue:28 msgid "Password updated successfully" msgstr "Senha atualizada com sucesso" -#: front/src/components/audio/Player.vue:262 -msgid "Pause track" -msgstr "Parar música" - -#: front/src/components/federation/LibraryFollowTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:33 -#: front/src/components/library/import/BatchDetail.vue:69 -#: front/src/components/library/import/BatchList.vue:14 -#: front/src/components/library/import/FileUpload.vue:59 -#: front/src/components/manage/library/RequestsTable.vue:27 -#: front/src/components/manage/library/RequestsTable.vue:63 +#: front/src/components/library/FileUpload.vue:105 +#: front/src/views/content/libraries/FilesTable.vue:14 +#: front/src/views/content/libraries/FilesTable.vue:233 msgid "Pending" msgstr "Pendente" -#: front/src/components/federation/LibraryFollowTable.vue:11 -#: front/src/views/federation/LibraryDetail.vue:26 +#: front/src/views/content/libraries/Detail.vue:37 msgid "Pending approval" msgstr "Aprovação pendente" -#: front/src/components/Sidebar.vue:217 -msgid "Pending follow requests" -msgstr "" +#: front/src/views/content/libraries/Quota.vue:22 +msgid "Pending files" +msgstr "Ficheiros pendentes" -#: front/src/components/Sidebar.vue:216 src/views/admin/library/Base.vue:26 -msgid "Pending import requests" -msgstr "Pedidos de importação pendentes" - -#: front/src/components/requests/Form.vue:26 src/views/federation/Base.vue:36 +#: front/src/components/requests/Form.vue:26 msgid "Pending requests" msgstr "Solicitações pendentes" @@ -1549,14 +1184,14 @@ msgstr "Solicitações pendentes" msgid "Permissions" msgstr "Permissões" -#: front/src/components/audio/PlayButton.vue:9 src/components/library/Track.vue:30 -#, fuzzy +#: front/src/components/audio/PlayButton.vue:9 +#: src/components/library/Track.vue:30 msgid "Play" -msgstr "Tocar" +msgstr "Jogar" #: front/src/components/audio/album/Card.vue:50 -#: front/src/components/audio/artist/Card.vue:44 src/components/library/Album.vue:28 -#: front/src/views/playlists/Detail.vue:23 +#: front/src/components/audio/artist/Card.vue:44 +#: src/components/library/Album.vue:28 front/src/views/playlists/Detail.vue:23 msgid "Play all" msgstr "Jogar tudo" @@ -1564,10 +1199,6 @@ msgstr "Jogar tudo" msgid "Play all albums" msgstr "Jogar todos os álbuns" -#: front/src/components/audio/PlayButton.vue:58 -msgid "Play immediatly" -msgstr "Jogar imediatamente" - #: front/src/components/audio/PlayButton.vue:15 msgid "Play next" msgstr "Jogar seguinte" @@ -1576,14 +1207,6 @@ msgstr "Jogar seguinte" msgid "Play now" msgstr "Jogue agora" -#: front/src/components/audio/Player.vue:261 -msgid "Play track" -msgstr "Jogar mũsica" - -#: front/src/views/playlists/Detail.vue:90 -msgid "Playlist" -msgstr "Playlist" - #: front/src/views/playlists/Detail.vue:12 msgid "Playlist containing %{ count } track, by %{ username }" msgid_plural "Playlist containing %{ count } tracks, by %{ username }" @@ -1610,9 +1233,9 @@ msgstr "Playlist atualizada" msgid "Playlist visibility" msgstr "Visibilidade da playlist" -#: front/src/components/Sidebar.vue:56 src/components/library/Home.vue:16 -#: front/src/components/library/Library.vue:13 src/views/admin/Settings.vue:83 -#: front/src/views/playlists/List.vue:103 +#: front/src/components/Sidebar.vue:71 src/components/library/Home.vue:16 +#: front/src/components/library/Library.vue:13 src/views/playlists/List.vue:104 +#: front/src/views/admin/Settings.vue:82 msgid "Playlists" msgstr "Playlists" @@ -1626,817 +1249,1398 @@ msgstr "Por favor, verifique novamente se sua senha está correta" #: front/src/components/auth/Login.vue:9 msgid "Please double-check your username/password couple is correct" -msgstr "" +msgstr "Por favor, verifique se o seu nome de usuário e senha estão corretos" #: front/src/components/auth/Settings.vue:46 msgid "PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px." -msgstr "" +msgstr "PNG, GIF ou JPG. No máximo 2MB. Será reduzido para 400x400px." -#: front/src/components/library/import/Main.vue:26 -msgid "Previous step" -msgstr "" - -#: front/src/components/audio/Player.vue:260 -msgid "Previous track" -msgstr "" +#: front/src/components/library/FileUpload.vue:58 +msgid "Proceed" +msgstr "Prosseguir" #: front/src/views/auth/EmailConfirm.vue:26 #: front/src/views/auth/PasswordResetConfirm.vue:31 msgid "Proceed to login" -msgstr "" +msgstr "Continuar com o login" -#: front/src/components/federation/LibraryTrackTable.vue:50 -msgid "Published date" -msgstr "" +#: front/src/components/library/FileUpload.vue:17 +msgid "Processing" +msgstr "Em tratamento" -#: front/src/components/library/import/ArtistImport.vue:17 -msgid "Query template" -msgstr "" +#: front/src/views/content/libraries/Quota.vue:36 +#: front/src/views/content/libraries/Quota.vue:39 +#: front/src/views/content/libraries/Quota.vue:62 +#: front/src/views/content/libraries/Quota.vue:65 +#: front/src/views/content/libraries/Quota.vue:88 +#: front/src/views/content/libraries/Quota.vue:91 +msgid "Purge" +msgstr "Purga" + +#: front/src/views/content/libraries/Quota.vue:89 +msgid "Purge errored files?" +msgstr "Limpar arquivos com erros?" + +#: front/src/views/content/libraries/Quota.vue:37 +msgid "Purge pending files?" +msgstr "Remover arquivos pendentes?" + +#: front/src/views/content/libraries/Quota.vue:63 +msgid "Purge skipped files?" +msgstr "Limpar arquivos ignorados?" #: front/src/components/Sidebar.vue:20 msgid "Queue" -msgstr "" +msgstr "Fila" -#: front/src/components/audio/Player.vue:203 -msgid "Queue shuffled!" -msgstr "" +#: front/src/components/library/radios/Builder.vue:15 +msgid "Radio created" +msgstr "Rádio criado" -#: front/src/views/radios/Detail.vue:80 -msgid "Radio" -msgstr "" - -#: front/src/components/library/radios/Builder.vue:205 -msgid "Radio Builder" -msgstr "" - -#: front/src/components/library/radios/Builder.vue:11 +#: front/src/components/library/radios/Builder.vue:21 msgid "Radio name" -msgstr "" +msgstr "Nome do rádio" -#: front/src/components/library/Library.vue:10 src/components/library/Radios.vue:141 +#: front/src/components/library/radios/Builder.vue:12 +msgid "Radio updated" +msgstr "Rádio atualizado" + +#: front/src/components/library/Library.vue:10 +#: src/components/library/Radios.vue:142 msgid "Radios" -msgstr "" +msgstr "Rádios" #: front/src/views/instance/Timeline.vue:7 msgid "Recent activity on this instance" -msgstr "" +msgstr "Atividade recente nesta instância" #: front/src/components/library/Home.vue:24 msgid "Recently added" -msgstr "" +msgstr "Adicionado recentemente" #: front/src/components/library/Home.vue:11 msgid "Recently favorited" -msgstr "" +msgstr "Recentemente adicionado aos favoritos" #: front/src/components/library/Home.vue:6 msgid "Recently listened" -msgstr "" +msgstr "Escutei recentemente" -#: front/src/components/library/import/BatchDetail.vue:81 -msgid "Recording MusicBrainz ID" -msgstr "" - -#: front/src/views/admin/users/UsersDetail.vue:84 -#: front/src/views/federation/LibraryDetail.vue:122 +#: front/src/views/admin/users/UsersDetail.vue:103 +#: front/src/views/content/remote/Home.vue:15 msgid "Refresh" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:42 -msgid "Refused" -msgstr "" +msgstr "Atualizar" #: front/src/components/auth/Profile.vue:12 msgid "Registered since %{ date }" -msgstr "" +msgstr "Registrado desde %{ date }" #: front/src/components/auth/Signup.vue:9 msgid "Registration are closed on this instance, you will need an invitation code to signup." msgstr "" +"As inscrições estão fechadas nesta instância, você precisará de um código de " +"convite para inscrição." #: front/src/components/manage/users/UsersTable.vue:71 msgid "regular user" +msgstr "usuário regular" + +#: front/src/views/content/libraries/Detail.vue:51 +msgid "Reject" +msgstr "Rejeitar" + +#: front/src/views/content/libraries/Detail.vue:43 +msgid "Rejected" +msgstr "Rejeitado" + +#: front/src/views/content/remote/Home.vue:6 +msgid "Remote libraries" +msgstr "Bibliotecas Remotas" + +#: front/src/views/content/remote/Home.vue:7 +msgid "Remote libraries are owned by other users on the network. You can access them as long as they are public or you are granted access." msgstr "" +"Bibliotecas remotas são de propriedade de outros usuários na rede. Você pode " +"acessá-los desde que sejam públicos ou tenha acesso." #: front/src/components/library/radios/Filter.vue:59 msgid "Remove" -msgstr "" +msgstr "Remover" #: front/src/components/auth/Settings.vue:58 msgid "Remove avatar" -msgstr "" - -#: front/src/components/favorites/TrackFavoriteIcon.vue:19 -msgid "Remove from favorites" -msgstr "" +msgstr "Remover avatar" #: front/src/components/auth/SubsonicTokenForm.vue:34 #: front/src/components/auth/SubsonicTokenForm.vue:37 msgid "Request a new password" -msgstr "" +msgstr "Solicite uma nova senha" #: front/src/components/auth/SubsonicTokenForm.vue:35 msgid "Request a new Subsonic API password?" -msgstr "" +msgstr "Solicitar uma nova senha da Subsonic API?" #: front/src/components/auth/SubsonicTokenForm.vue:43 msgid "Request a password" -msgstr "" - -#: front/src/App.vue:35 -msgid "Request music" -msgstr "" - -#: front/src/views/library/MusicRequest.vue:4 src/views/library/MusicRequest.vue:21 -msgid "Request some music" -msgstr "" +msgstr "Solicite uma senha" #: front/src/components/requests/Form.vue:20 msgid "Request submitted!" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:49 -msgid "Rerun errored jobs" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:187 -msgid "Rerun job" -msgstr "" +msgstr "Solicitação Enviada!" #: front/src/components/auth/Login.vue:34 src/views/auth/PasswordReset.vue:4 -#: front/src/views/auth/PasswordReset.vue:52 msgid "Reset your password" -msgstr "" +msgstr "Redefinir sua senha" -#: front/src/components/library/import/TrackImport.vue:31 -msgid "Result %{ current }/%{ total }" -msgstr "" - -#: front/src/components/favorites/List.vue:38 src/components/library/Artists.vue:30 -#: front/src/components/library/Radios.vue:52 src/views/federation/LibraryList.vue:33 -#: front/src/views/playlists/List.vue:32 +#: front/src/components/favorites/List.vue:38 +#: src/components/library/Artists.vue:30 +#: front/src/components/library/Radios.vue:52 src/views/playlists/List.vue:32 msgid "Results per page" -msgstr "" +msgstr "Resultados por página" #: front/src/components/admin/SettingsGroup.vue:63 -#: front/src/components/library/radios/Builder.vue:19 +#: front/src/components/library/radios/Builder.vue:33 msgid "Save" +msgstr "Salvar" + +#: front/src/views/content/remote/Card.vue:31 +msgid "Scan pending" +msgstr "Esperando pela análise" + +#: front/src/views/content/remote/Card.vue:43 +msgid "Scanned successfully" +msgstr "Analisado com sucesso" + +#: front/src/views/content/remote/Card.vue:47 +msgid "Scanned with errors" +msgstr "Analisado com erros" + +#: front/src/views/content/remote/Card.vue:35 +msgid "Scanning... (%{ progress }%)" msgstr "" -#: front/src/views/federation/LibraryDetail.vue:112 -msgid "Scan triggered!" -msgstr "" - -#: front/src/components/federation/LibraryTrackTable.vue:5 -#: front/src/components/library/Artists.vue:10 src/components/library/Radios.vue:29 -#: front/src/components/library/import/BatchDetail.vue:62 -#: front/src/components/library/import/BatchList.vue:7 +#: front/src/components/library/Artists.vue:10 +#: src/components/library/Radios.vue:29 #: front/src/components/manage/library/FilesTable.vue:5 -#: front/src/components/manage/library/RequestsTable.vue:5 #: front/src/components/manage/users/InvitationsTable.vue:5 #: front/src/components/manage/users/UsersTable.vue:5 -#: front/src/views/federation/LibraryList.vue:14 src/views/playlists/List.vue:13 +#: front/src/views/content/libraries/FilesTable.vue:5 +#: src/views/playlists/List.vue:13 msgid "Search" -msgstr "" +msgstr "Buscar" -#: front/src/components/library/import/Main.vue:85 -msgid "Search an entity you want to import:" -msgstr "" - -#: front/src/components/manage/library/RequestsTable.vue:180 -msgid "Search by artist, username, comment..." -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:188 -msgid "Search by source..." -msgstr "" - -#: front/src/components/library/import/BatchList.vue:116 -msgid "Search by submitter, source..." -msgstr "" - -#: front/src/components/federation/LibraryTrackTable.vue:182 -#: front/src/components/manage/library/FilesTable.vue:175 -msgid "Search by title, artist, domain..." -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:140 -msgid "Search by username, domain..." -msgstr "" - -#: front/src/components/manage/users/InvitationsTable.vue:152 -msgid "Search by username, email, code..." -msgstr "" - -#: front/src/components/manage/users/UsersTable.vue:162 -msgid "Search by username, email, name..." -msgstr "" - -#: front/src/components/audio/SearchBar.vue:20 -msgid "Search for artists, albums, tracks..." -msgstr "" +#: front/src/views/content/remote/ScanForm.vue:9 +msgid "Search a remote library" +msgstr "Pesquisar uma biblioteca remota" #: front/src/components/audio/Search.vue:2 msgid "Search for some music" -msgstr "" +msgstr "Procure alguma música" -#: front/src/components/library/Track.vue:105 +#: front/src/components/library/Track.vue:116 msgid "Search on lyrics.wikia.com" -msgstr "" +msgstr "Procurar em lyrics.wikia.com" -#: front/src/components/library/Album.vue:33 src/components/library/Artist.vue:31 +#: front/src/components/library/Album.vue:33 +#: src/components/library/Artist.vue:31 #: front/src/components/library/Track.vue:40 msgid "Search on Wikipedia" -msgstr "" - -#: front/src/components/library/import/TrackImport.vue:42 -msgid "Search query" -msgstr "" +msgstr "Procurar em Wikipedia" #: front/src/views/admin/Settings.vue:15 msgid "Sections" msgstr "" -#: front/src/components/library/radios/Builder.vue:31 +#: front/src/components/library/radios/Builder.vue:45 msgid "Select a filter" -msgstr "" +msgstr "Selecione um filtro" -#: front/src/components/common/ActionTable.vue:61 +#: front/src/components/common/ActionTable.vue:64 msgid "Select all %{ total } elements" msgid_plural "Select all %{ total } elements" msgstr[0] "" msgstr[1] "" -#: front/src/components/library/import/FileUpload.vue:22 -msgid "Select files to upload..." -msgstr "" - -#: front/src/components/common/ActionTable.vue:69 +#: front/src/components/common/ActionTable.vue:73 msgid "Select only current page" -msgstr "" +msgstr "Selecione apenas a página atual" -#: front/src/components/library/import/Main.vue:19 -msgid "Select relevant sources or files for import" -msgstr "" - -#: front/src/components/federation/LibraryCard.vue:43 -msgid "Send a follow request" -msgstr "" - -#: front/src/components/Sidebar.vue:97 src/components/manage/users/UsersTable.vue:184 -#: front/src/views/admin/users/UsersDetail.vue:165 +#: front/src/components/Sidebar.vue:43 src/components/Sidebar.vue:88 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 msgid "Settings" -msgstr "" +msgstr "Configurações" #: front/src/components/auth/Settings.vue:10 msgid "Settings updated" -msgstr "" +msgstr "Configurações atualizadas" #: front/src/components/admin/SettingsGroup.vue:11 msgid "Settings updated successfully." -msgstr "" - -#: front/src/components/auth/Profile.vue:24 -msgid "Settings..." -msgstr "" +msgstr "Configurações atualizadas com sucesso." #: front/src/components/manage/users/InvitationForm.vue:27 msgid "Share link" +msgstr "Compartilhe o link" + +#: front/src/views/content/libraries/Detail.vue:15 +msgid "Share this link with other users so they can request an access to your library." msgstr "" +"Compartilhe este link com outros usuários para que eles possam solicitar " +"acesso à sua biblioteca." + +#: front/src/views/content/libraries/Detail.vue:14 +#: front/src/views/content/remote/Card.vue:73 +msgid "Sharing link" +msgstr "" + +#: front/src/components/audio/album/Card.vue:40 +#, fuzzy +msgid "Show %{ count } more track" +msgid_plural "Show %{ count } more tracks" +msgstr[0] "%{ count } tema" +msgstr[1] "%{ count } temas" #: front/src/components/audio/artist/Card.vue:30 msgid "Show 1 more album" msgid_plural "Show %{ count } more albums" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Mostrar mais 1 álbum" +msgstr[1] "Mostrar mais %{ count } albums" -#: front/src/components/audio/album/Card.vue:40 -msgid "Show 1 more track" -msgid_plural "Show %{ count } more tracks" -msgstr[0] "" -msgstr[1] "" +#: front/src/views/Notifications.vue:10 +msgid "Show read notifications" +msgstr "Mostrar notificações" -#: front/src/components/forms/PasswordInput.vue:25 -msgid "Show/hide password" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:97 -#: front/src/components/federation/LibraryTrackTable.vue:98 -#: front/src/components/library/import/BatchDetail.vue:128 -#: front/src/components/library/import/BatchList.vue:73 #: front/src/components/manage/library/FilesTable.vue:97 -#: front/src/components/manage/library/RequestsTable.vue:104 #: front/src/components/manage/users/InvitationsTable.vue:76 #: front/src/components/manage/users/UsersTable.vue:87 +#: front/src/views/content/libraries/FilesTable.vue:111 msgid "Showing results %{ start }-%{ end } on %{ total }" -msgstr "" - -#: front/src/components/audio/Player.vue:269 -msgid "Shuffle your queue" -msgstr "" - -#: front/src/components/auth/Signup.vue:95 -msgid "Sign Up" -msgstr "" +msgstr "Mostrando resultados %{ start }-%{ end } em %{ total }" #: front/src/components/manage/users/UsersTable.vue:40 #: front/src/views/admin/users/UsersDetail.vue:37 msgid "Sign-up" -msgstr "" +msgstr "Inscrever-se" -#: front/src/components/audio/track/Table.vue:31 -msgid "Simply copy paste the snippet below into a terminal to launch the download." -msgstr "" - -#: front/src/components/library/Track.vue:69 -#: front/src/components/library/import/FileUpload.vue:44 +#: front/src/components/library/FileUpload.vue:84 +#: src/components/library/Track.vue:69 #: front/src/components/manage/library/FilesTable.vue:44 +#: front/src/views/content/libraries/FilesTable.vue:57 +#: front/src/components/mixins/Translations.vue:27 msgid "Size" -msgstr "" +msgstr "Tamanho" -#: front/src/components/library/import/BatchDetail.vue:37 -#: front/src/components/library/import/BatchDetail.vue:72 +#: front/src/views/content/libraries/FilesTable.vue:15 +#: front/src/views/content/libraries/FilesTable.vue:229 +#, fuzzy msgid "Skipped" -msgstr "" +msgstr "Pulou" + +#: front/src/views/content/libraries/Quota.vue:49 +msgid "Skipped files" +msgstr "Arquivos ignorados" #: front/src/components/requests/Form.vue:3 msgid "Something's missing in the library? Let us know what you would like to listen!" msgstr "" +"Algo está faltando na biblioteca? Deixe-nos saber o que você gostaria de " +"ouvir!" #: front/src/components/audio/Search.vue:25 msgid "Sorry, we did not found any album matching your query" -msgstr "" +msgstr "Desculpe, não foi encontrado nenhum álbum correspondente à sua consulta" #: front/src/components/audio/Search.vue:16 msgid "Sorry, we did not found any artist matching your query" msgstr "" +"Desculpe, não foi encontrado nenhum artista que corresponda à sua consulta" -#: front/src/components/library/import/BatchDetail.vue:82 -#: front/src/components/library/import/BatchList.vue:38 -#: front/src/components/library/import/TrackImport.vue:17 -msgid "Source" -msgstr "" - -#: front/src/App.vue:41 +#: front/src/App.vue:40 msgid "Source code" msgstr "" -#: front/src/App.vue:40 +#: front/src/App.vue:39 msgid "Source code (%{version})" msgstr "" -#: front/src/components/auth/Profile.vue:20 +#: front/src/components/auth/Profile.vue:23 #: front/src/components/manage/users/UsersTable.vue:70 msgid "Staff member" -msgstr "" +msgstr "Membro da equipe" #: front/src/components/radios/Button.vue:4 msgid "Start" -msgstr "" +msgstr "Iniciar" -#: front/src/components/library/import/FileUpload.vue:28 -msgid "Start Upload" -msgstr "" - -#: front/src/views/admin/Settings.vue:86 -msgid "Statistics" -msgstr "" - -#: front/src/components/federation/LibraryFollowTable.vue:23 -#: front/src/components/federation/LibraryTrackTable.vue:46 -#: front/src/components/library/import/BatchDetail.vue:66 -#: front/src/components/library/import/BatchDetail.vue:83 -#: front/src/components/library/import/BatchList.vue:11 -#: front/src/components/library/import/BatchList.vue:37 -#: front/src/components/library/import/FileUpload.vue:45 -#: front/src/components/manage/library/RequestsTable.vue:24 -#: front/src/components/manage/library/RequestsTable.vue:48 +#: front/src/components/library/FileUpload.vue:85 #: front/src/components/manage/users/InvitationsTable.vue:17 #: front/src/components/manage/users/InvitationsTable.vue:39 #: front/src/components/manage/users/UsersTable.vue:43 +#: front/src/views/content/libraries/Detail.vue:28 msgid "Status" msgstr "" #: front/src/components/radios/Button.vue:3 msgid "Stop" -msgstr "" +msgstr "Pare" -#: front/src/components/Sidebar.vue:159 +#: front/src/components/Sidebar.vue:150 msgid "Stop radio" -msgstr "" +msgstr "Pare o rádio" -#: front/src/components/library/import/FileUpload.vue:32 -msgid "Stop Upload" -msgstr "" - -#: front/src/App.vue:9 src/components/requests/Form.vue:17 +#: front/src/App.vue:11 src/components/requests/Form.vue:17 msgid "Submit" -msgstr "" +msgstr "Enviar" #: front/src/components/requests/Form.vue:22 msgid "Submit another request" -msgstr "" - -#: front/src/components/library/import/BatchDetail.vue:26 -#: front/src/components/library/import/BatchList.vue:39 -msgid "Submitted by" -msgstr "" - -#: front/src/views/admin/Settings.vue:85 -msgid "Subsonic" -msgstr "" +msgstr "Envie outro pedido" #: front/src/components/auth/SubsonicTokenForm.vue:2 msgid "Subsonic API password" -msgstr "" +msgstr "Senha da API Subsonic" -#: front/src/components/library/import/BatchDetail.vue:71 -#: front/src/components/library/import/BatchList.vue:16 -#: front/src/components/library/import/FileUpload.vue:56 -msgid "Success" -msgstr "" - -#: front/src/App.vue:11 +#: front/src/App.vue:13 msgid "Suggested choices" -msgstr "" +msgstr "Escolhas sugeridas" + +#: front/src/components/library/FileUpload.vue:3 +msgid "Summary" +msgstr "Sumário" #: front/src/components/playlists/Editor.vue:9 msgid "Syncing changes to server..." msgstr "" +#: front/src/components/common/CopyInput.vue:3 +msgid "Text copied to clipboard!" +msgstr "Texto copiado para a área de transferência!" + #: front/src/components/Home.vue:26 msgid "That's simple: we loved Grooveshark and we want to build something even better." msgstr "" +"Isso é simples: nós amamos o Grooveshark e queremos construir algo ainda " +"melhor." -#: front/src/components/requests/Form.vue:73 -msgid "The Beatles, Mickael Jackson…" -msgstr "" - -#: front/src/App.vue:59 +#: front/src/App.vue:58 msgid "The funkwhale logo was kindly designed and provided by Francis Gading." msgstr "" +"O logótipo funkwhale foi gentilmente projetado e fornecido por Francis " +"Gading." -#: front/src/components/Home.vue:124 +#: front/src/views/content/libraries/Form.vue:34 +msgid "The library and all its tracks will be deleted. This action is irreversible." +msgstr "" + +#: front/src/components/library/FileUpload.vue:39 +msgid "The music files you are uploading are tagged properly:" +msgstr "Os arquivos de música que você está enviando são marcados corretamente:" + +#: front/src/components/Home.vue:121 msgid "The plaform is free and open-source, you can install it and modify it without worries" msgstr "" +"A plataforma é gratuita e open-source, você pode instalá-lo e modificá-lo " +"sem preocupações" #: front/src/components/auth/SubsonicTokenForm.vue:4 msgid "The Subsonic API is not available on this Funkwhale instance." -msgstr "" +msgstr "A API Subsonic não está disponível nesta instância do Funkwhale." -#: front/src/components/requests/Form.vue:74 -msgid "The White Album, Thriller…" -msgstr "" +#: front/src/components/library/FileUpload.vue:43 +msgid "The uploaded music files are in OGG, Flac or MP3 format" +msgstr "Os arquivos de música enviados estão no formato OGG, Flac ou MP3" -#: front/src/components/audio/track/Table.vue:30 -msgid "There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as cURL to easily download a list of tracks." -msgstr "" +#: front/src/components/library/Album.vue:52 +msgid "This album is present in the following libraries:" +msgstr "Este álbum está presente nas seguintes bibliotecas:" -#: front/src/components/library/import/Main.vue:149 -msgid "This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled." -msgstr "" +#: front/src/components/library/Artist.vue:63 +msgid "This artist is present in the following libraries:" +msgstr "Este artista está presente nas seguintes bibliotecas:" -#: front/src/views/federation/LibraryDetail.vue:195 -msgid "This indicate if the remote library granted you access" +#: front/src/views/content/Home.vue:9 +msgid "This instance offers up to %{quota} of storage space to every user." msgstr "" +"Esta instância oferece até %{quota} de espaço de armazenamento para todos os " +"usuários." #: front/src/components/auth/Profile.vue:16 msgid "This is you!" -msgstr "" +msgstr "É você!" -#: front/src/components/common/ActionTable.vue:37 +#: front/src/components/common/ActionTable.vue:38 msgid "This may affect a lot of elements, please double check this is really what you want." msgstr "" +"Isso pode afetar muitos elementos, por favor, verifique isso é realmente o " +"que você quer." -#: front/src/components/audio/PlayButton.vue:61 -msgid "This track is not imported and cannot be played" -msgstr "" +#: front/src/components/library/FileUpload.vue:52 +msgid "This reference will be used to group imported files together." +msgstr "Esta referência será usada para agrupar arquivos importados." + +#: front/src/components/library/Track.vue:125 +msgid "This track is present in the following libraries:" +msgstr "Esta música está presente nas seguintes bibliotecas:" #: front/src/views/playlists/Detail.vue:37 msgid "This will completely delete this playlist and cannot be undone." -msgstr "" +msgstr "Isso excluirá completamente essa playlist e não poderá ser desfeito." #: front/src/views/radios/Detail.vue:27 msgid "This will completely delete this radio and cannot be undone." -msgstr "" +msgstr "Isto irá remover completamente o rádio e não pode ser cancelado." #: front/src/components/auth/SubsonicTokenForm.vue:51 msgid "This will completely disable access to the Subsonic API using from account." msgstr "" +"Isso desativará completamente o acesso à API do Subsonic usando a conta." -#: front/src/App.vue:133 +#: front/src/App.vue:162 src/components/About.vue:55 +#: src/components/Home.vue:154 front/src/components/PageNotFound.vue:33 +#: src/components/Sidebar.vue:203 front/src/components/Sidebar.vue:204 +#: src/components/audio/PlayButton.vue:54 +#: front/src/components/audio/PlayButton.vue:55 +#: front/src/components/audio/PlayButton.vue:56 +#: front/src/components/audio/PlayButton.vue:61 +#: front/src/components/audio/PlayButton.vue:64 +#: front/src/components/audio/PlayButton.vue:158 +#: src/components/audio/Player.vue:216 +#: front/src/components/audio/Player.vue:273 +#: src/components/audio/Player.vue:274 +#: front/src/components/audio/Player.vue:275 +#: src/components/audio/Player.vue:276 +#: front/src/components/audio/Player.vue:277 +#: src/components/audio/Player.vue:278 +#: front/src/components/audio/Player.vue:279 +#: src/components/audio/Player.vue:280 +#: front/src/components/audio/Player.vue:281 +#: src/components/audio/Player.vue:282 +#: front/src/components/audio/Player.vue:283 src/components/audio/Search.vue:65 +#: front/src/components/audio/SearchBar.vue:20 +#: src/components/audio/SearchBar.vue:25 +#: front/src/components/audio/SearchBar.vue:26 +#: src/components/audio/SearchBar.vue:27 front/src/components/auth/Login.vue:77 +#: src/components/auth/Login.vue:78 front/src/components/auth/Logout.vue:20 +#: src/components/auth/Profile.vue:47 +#: front/src/components/auth/Settings.vue:249 src/components/auth/Signup.vue:95 +#: front/src/components/auth/Signup.vue:96 src/components/auth/Signup.vue:97 +#: front/src/components/auth/Signup.vue:98 +#: front/src/components/auth/SubsonicTokenForm.vue:95 +#: front/src/components/auth/SubsonicTokenForm.vue:111 +#: front/src/components/favorites/List.vue:110 +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +#: front/src/components/favorites/TrackFavoriteIcon.vue:21 +#: front/src/components/forms/PasswordInput.vue:25 +#: front/src/components/library/Album.vue:101 +#: src/components/library/Artist.vue:129 +#: front/src/components/library/Artists.vue:119 +#: front/src/components/library/Artists.vue:120 +#: front/src/components/library/FileUpload.vue:238 +#: front/src/components/library/FileUpload.vue:239 +#: front/src/components/library/FileUpload.vue:240 +#: front/src/components/library/FileUpload.vue:241 +#: src/components/library/Home.vue:65 +#: front/src/components/library/Radios.vue:141 +#: src/components/library/Radios.vue:142 +#: front/src/components/library/Track.vue:195 +#: front/src/components/library/radios/Builder.vue:233 +#: front/src/components/library/radios/Builder.vue:235 +#: front/src/components/library/radios/Builder.vue:236 +#: front/src/components/manage/library/FilesTable.vue:176 +#: front/src/components/manage/library/FilesTable.vue:190 +#: front/src/components/manage/users/InvitationForm.vue:58 +#: front/src/components/manage/users/InvitationsTable.vue:153 +#: front/src/components/manage/users/InvitationsTable.vue:167 +#: front/src/components/manage/users/UsersTable.vue:163 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/components/manage/users/UsersTable.vue:177 +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/components/manage/users/UsersTable.vue:185 +#: front/src/components/metadata/ArtistCard.vue:49 +#: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/Search.vue:114 +#: front/src/components/metadata/Search.vue:130 +#: front/src/components/metadata/Search.vue:134 +#: front/src/components/metadata/Search.vue:138 +#: front/src/components/mixins/Translations.vue:7 +#: front/src/components/mixins/Translations.vue:8 +#: front/src/components/mixins/Translations.vue:10 +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/mixins/Translations.vue:16 +#: front/src/components/mixins/Translations.vue:17 +#: front/src/components/mixins/Translations.vue:18 +#: front/src/components/mixins/Translations.vue:19 +#: front/src/components/mixins/Translations.vue:20 +#: front/src/components/mixins/Translations.vue:21 +#: front/src/components/mixins/Translations.vue:22 +#: front/src/components/mixins/Translations.vue:23 +#: front/src/components/mixins/Translations.vue:24 +#: front/src/components/mixins/Translations.vue:25 +#: front/src/components/mixins/Translations.vue:26 +#: front/src/components/mixins/Translations.vue:27 +#: front/src/components/mixins/Translations.vue:28 +#: front/src/components/mixins/Translations.vue:29 +#: front/src/components/mixins/Translations.vue:30 +#: front/src/components/mixins/Translations.vue:31 +#: front/src/components/notifications/NotificationRow.vue:39 +#: front/src/components/notifications/NotificationRow.vue:40 +#: front/src/components/notifications/NotificationRow.vue:44 +#: front/src/components/notifications/NotificationRow.vue:45 +#: front/src/components/notifications/NotificationRow.vue:62 +#: front/src/components/playlists/Editor.vue:163 +#: src/components/playlists/Form.vue:74 +#: front/src/components/playlists/Form.vue:81 +#: src/components/playlists/Form.vue:85 +#: front/src/components/playlists/Form.vue:89 +#: front/src/components/playlists/PlaylistModal.vue:116 +#: front/src/components/playlists/TrackPlaylistIcon.vue:32 +#: front/src/components/requests/Form.vue:73 +#: src/components/requests/Form.vue:74 +#: front/src/components/requests/Form.vue:75 src/views/Notifications.vue:74 +#: front/src/views/admin/Settings.vue:75 src/views/admin/Settings.vue:80 +#: front/src/views/admin/Settings.vue:81 src/views/admin/Settings.vue:82 +#: front/src/views/admin/Settings.vue:83 src/views/admin/Settings.vue:84 +#: front/src/views/admin/Settings.vue:85 src/views/admin/Settings.vue:86 +#: front/src/views/admin/library/Base.vue:16 +#: src/views/admin/library/FilesList.vue:21 +#: front/src/views/admin/users/Base.vue:20 +#: front/src/views/admin/users/InvitationsList.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:169 +#: front/src/views/admin/users/UsersDetail.vue:170 +#: front/src/views/admin/users/UsersDetail.vue:177 +#: front/src/views/admin/users/UsersDetail.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: front/src/views/admin/users/UsersDetail.vue:189 +#: front/src/views/admin/users/UsersList.vue:21 +#: src/views/auth/EmailConfirm.vue:51 front/src/views/auth/PasswordReset.vue:52 +#: src/views/auth/PasswordReset.vue:53 +#: front/src/views/auth/PasswordResetConfirm.vue:62 +#: src/views/content/Base.vue:19 front/src/views/content/Home.vue:35 +#: src/views/content/libraries/Card.vue:58 +#: front/src/views/content/libraries/Card.vue:59 +#: front/src/views/content/libraries/Card.vue:60 +#: front/src/views/content/libraries/Card.vue:61 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/content/libraries/FilesTable.vue:226 +#: front/src/views/content/libraries/FilesTable.vue:229 +#: front/src/views/content/libraries/FilesTable.vue:230 +#: front/src/views/content/libraries/FilesTable.vue:233 +#: front/src/views/content/libraries/FilesTable.vue:234 +#: front/src/views/content/libraries/FilesTable.vue:237 +#: front/src/views/content/libraries/FilesTable.vue:238 +#: front/src/views/content/libraries/FilesTable.vue:241 +#: front/src/views/content/libraries/FilesTable.vue:242 +#: front/src/views/content/libraries/FilesTable.vue:258 +#: front/src/views/content/libraries/FilesTable.vue:259 +#: front/src/views/content/libraries/Form.vue:70 +#: front/src/views/content/libraries/Form.vue:71 +#: front/src/views/content/libraries/Form.vue:72 +#: front/src/views/content/libraries/Form.vue:73 +#: front/src/views/content/libraries/Form.vue:74 +#: front/src/views/content/libraries/Form.vue:106 +#: front/src/views/content/libraries/Form.vue:109 +#: front/src/views/content/libraries/Form.vue:129 +#: front/src/views/content/remote/Card.vue:131 +#: src/views/content/remote/Card.vue:132 +#: front/src/views/content/remote/Card.vue:165 +#: src/views/content/remote/Card.vue:166 +#: front/src/views/content/remote/ScanForm.vue:48 +#: src/views/instance/Timeline.vue:57 front/src/views/playlists/Detail.vue:90 +#: src/views/playlists/List.vue:104 front/src/views/playlists/List.vue:105 +#: src/views/radios/Detail.vue:80 msgid "This will erase your local data and disconnect you, do you want to continue?" -msgstr "" +msgid_plural "%{ count } tracks were added to your queue" +msgstr[0] "Isso apagará seus dados locais e desconectará você. Deseja continuar?" +msgstr[1] "%{ count } músicas foram adicionadas à sua fila" #: front/src/components/auth/SubsonicTokenForm.vue:36 msgid "This will log you out from existing devices that use the current password." msgstr "" +"Isto irá desconectá-lo a partir de dispositivos existentes que usam a senha " +"atual." #: front/src/components/playlists/Editor.vue:44 msgid "This will remove all tracks from this playlist and cannot be undone." msgstr "" +"Isso removerá todas as músicas dessa playlist e não poderá ser desfeito." + +#: front/src/views/content/libraries/Quota.vue:90 +msgid "This will remove tracks that were uploaded but failed to be process by the server. This will remove those files completely and you will regain the corresponding quota." +msgstr "" +"Isso removerá as músicas que foram enviadas, mas não foram processadas pelo " +"servidor. Isso removerá esses arquivos completamente e você recuperará a " +"quota correspondente." + +#: front/src/views/content/libraries/Quota.vue:38 +msgid "This will remove tracks that were uploaded but not processed yet. This will remove those files completely and you will regain the corresponding quota." +msgstr "" +"Isso removerá as músicas que foram enviadas, mas ainda não foram " +"processadas. Isso removerá esses arquivos completamente e você recuperará a " +"quota correspondente." + +#: front/src/views/content/libraries/Quota.vue:64 +msgid "This will remove tracks that were uploaded but skipped during import processes for various reasons. This will remove those files completely and you will regain the corresponding quota." +msgstr "" +"Isso removerá as músicas que foram carregadas, mas foram ignoradas durante " +"os processos de importação por vários motivos. Isso removerá esses arquivos " +"completamente e você recuperará a quota correspondente." #: front/src/components/audio/track/Table.vue:6 -#: front/src/components/federation/LibraryTrackTable.vue:47 #: front/src/components/manage/library/FilesTable.vue:37 +#: front/src/views/content/libraries/FilesTable.vue:51 +#: front/src/components/mixins/Translations.vue:26 msgid "Title" -msgstr "" - -#: front/src/components/audio/SearchBar.vue:27 src/components/library/Track.vue:174 -#: front/src/components/library/import/BatchDetail.vue:84 -#: front/src/components/metadata/Search.vue:138 -msgid "Track" -msgstr "" +msgstr "Título" #: front/src/components/library/Track.vue:53 msgid "Track information" -msgstr "" +msgstr "Informação da música" #: front/src/components/library/radios/Filter.vue:44 +#, fuzzy msgid "Track matching filter" -msgstr "" +msgstr "Filtro de correspondência de músicas" #: front/src/components/instance/Stats.vue:54 msgid "tracks" -msgstr "" +msgstr "músicas" #: front/src/components/library/Album.vue:43 #: front/src/components/playlists/PlaylistModal.vue:33 -#: front/src/views/federation/Base.vue:8 src/views/playlists/Detail.vue:50 -#: front/src/views/radios/Detail.vue:34 +#: src/views/content/Base.vue:8 front/src/views/content/libraries/Detail.vue:8 +#: src/views/playlists/Detail.vue:50 front/src/views/radios/Detail.vue:34 msgid "Tracks" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:125 -msgid "Tracks available in this library" -msgstr "" +msgstr "Músicas" #: front/src/components/library/Artist.vue:54 msgid "Tracks by this artist" -msgstr "" +msgstr "Musicas deste artista" #: front/src/components/instance/Stats.vue:25 msgid "Tracks favorited" -msgstr "" +msgstr "Músicas favoritas" #: front/src/components/instance/Stats.vue:19 msgid "tracks listened" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:109 -msgid "Trigger scan" -msgstr "" +msgstr "músicas escutadas" +#: front/src/components/library/Track.vue:91 #: front/src/components/manage/library/FilesTable.vue:41 msgid "Type" -msgstr "" +msgstr "Tipo" + +#: front/src/views/content/remote/Card.vue:100 +#: src/views/content/remote/Card.vue:105 +msgid "Unfollow" +msgstr "Deixar de seguir" + +#: front/src/views/content/remote/Card.vue:101 +msgid "Unfollow this library?" +msgstr "Deixar de seguir esta biblioteca?" #: front/src/components/About.vue:15 msgid "Unfortunately, owners of this instance did not yet take the time to complete this page." msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:95 -msgid "Unknown" -msgstr "" +"Infelizmente, os proprietários desta instância não ainda ter o tempo para " +"preencher esta página." #: front/src/components/Home.vue:37 msgid "Unlimited music" -msgstr "" - -#: front/src/components/audio/Player.vue:264 -msgid "Unmute" -msgstr "" +msgstr "Música ilimitada" #: front/src/components/auth/Settings.vue:50 msgid "Update avatar" -msgstr "" +msgstr "Atualizar avatar" + +#: front/src/views/content/libraries/Form.vue:25 +msgid "Update library" +msgstr "Atualizar biblioteca" #: front/src/components/playlists/Form.vue:33 msgid "Update playlist" -msgstr "" +msgstr "Atualizar playlist" #: front/src/components/auth/Settings.vue:27 msgid "Update settings" -msgstr "" +msgstr "Atualizar configurações" #: front/src/views/auth/PasswordResetConfirm.vue:21 msgid "Update your password" -msgstr "" +msgstr "Atualize sua senha" -#: front/src/components/manage/users/UsersTable.vue:172 -#: front/src/views/admin/users/UsersDetail.vue:153 +#: front/src/views/content/libraries/Card.vue:44 +#: front/src/components/manage/users/UsersTable.vue:173 +#: front/src/views/content/libraries/DetailArea.vue:24 +#: front/src/views/admin/users/UsersDetail.vue:177 msgid "Upload" -msgstr "" +msgstr "Carregar" #: front/src/components/auth/Settings.vue:45 msgid "Upload a new avatar" +msgstr "Carregar um novo avatar" + +#: front/src/views/content/Home.vue:6 +msgid "Upload audio content" +msgstr "Carregar conteúdo de áudio" + +#: front/src/views/content/libraries/FilesTable.vue:54 +msgid "Upload date" +msgstr "Data de upload" + +#: front/src/views/content/Home.vue:7 +msgid "Upload music files (mp3, ogg, flac, etc.) from your personal library directly from your browser to enjoy them here." +msgstr "" +"Carregue arquivos de música (mp3, ogg, flac, etc.) da sua biblioteca pessoal " +"diretamente de seu navegador para apreciá-los aqui." + +#: front/src/components/library/FileUpload.vue:31 +msgid "Upload new tracks" +msgstr "Carregar novas músicas" + +#: front/src/views/admin/users/UsersDetail.vue:82 +#, fuzzy +msgid "Upload quota" +msgstr "Quota de upload" + +#: front/src/components/library/FileUpload.vue:99 +msgid "Uploaded" msgstr "" -#: front/src/components/library/import/Main.vue:7 -msgid "Uploaded files or external source" +#: front/src/components/library/FileUpload.vue:5 +msgid "Uploading" msgstr "" -#: front/src/components/library/import/FileUpload.vue:57 +#: front/src/components/library/FileUpload.vue:102 msgid "Uploading..." msgstr "" -#: front/src/App.vue:45 +#: front/src/App.vue:44 msgid "Use another instance" -msgstr "" - -#: front/src/components/requests/Form.vue:75 -msgid "Use this comment box to add details to your request if needed" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:196 -msgid "Use this flag to enable/disable federation with this library" -msgstr "" +msgstr "Use outra instância" #: front/src/views/auth/PasswordReset.vue:12 msgid "Use this form to request a password reset. We will send an email to the given address with instructions to reset your password." msgstr "" - -#: front/src/components/federation/LibraryForm.vue:6 -msgid "Use this form to scan an instance and setup federation." -msgstr "" +"Use este formulário para solicitar uma redefinição de senha. Enviaremos um " +"email para o endereço fornecido com instruções para redefinir sua senha." #: front/src/components/manage/users/InvitationsTable.vue:49 msgid "Used" -msgstr "" +msgstr "Usado" -#: front/src/components/manage/library/RequestsTable.vue:47 +#: front/src/views/content/libraries/Detail.vue:26 msgid "User" -msgstr "" +msgstr "Usuário" #: front/src/components/instance/Stats.vue:5 msgid "User activity" -msgstr "" +msgstr "Atividade do usuário" + +#: front/src/components/library/Album.vue:49 +#: src/components/library/Artist.vue:60 +#: front/src/components/library/Track.vue:122 +msgid "User libraries" +msgstr "Bibliotecas do usuário" #: front/src/components/library/Radios.vue:20 msgid "User radios" -msgstr "" +msgstr "Rádios do usuário" #: front/src/components/auth/Signup.vue:19 #: front/src/components/manage/users/UsersTable.vue:37 +#: front/src/components/mixins/Translations.vue:32 msgid "Username" -msgstr "" +msgstr "Nome de usuário" #: front/src/components/auth/Login.vue:15 msgid "Username or email" -msgstr "" +msgstr "Nome de usuário ou email" #: front/src/components/instance/Stats.vue:13 msgid "users" -msgstr "" +msgstr "usuários" -#: front/src/components/Sidebar.vue:103 src/views/admin/Settings.vue:81 -#: front/src/views/admin/users/Base.vue:5 src/views/admin/users/UsersList.vue:3 -#: front/src/views/admin/users/UsersList.vue:21 +#: front/src/components/Sidebar.vue:94 src/views/admin/users/Base.vue:5 +#: front/src/views/admin/users/UsersList.vue:3 +#: src/views/admin/users/UsersList.vue:21 front/src/views/admin/Settings.vue:81 msgid "Users" -msgstr "" +msgstr "Usuários" -#: front/src/components/library/Album.vue:37 src/components/library/Artist.vue:35 +#: front/src/views/content/libraries/Quota.vue:29 +#: front/src/views/content/libraries/Quota.vue:56 +#: front/src/views/content/libraries/Quota.vue:82 +msgid "View files" +msgstr "Ver ficheiros" + +#: front/src/components/library/Album.vue:37 +#: src/components/library/Artist.vue:35 #: front/src/components/library/Track.vue:44 -#: front/src/components/library/import/ArtistImport.vue:131 -#: front/src/components/metadata/ArtistCard.vue:49 #: front/src/components/metadata/ReleaseCard.vue:53 +#: front/src/components/metadata/ArtistCard.vue:49 msgid "View on MusicBrainz" -msgstr "" +msgstr "Ver no MusicBrainz" + +#: front/src/views/content/libraries/Form.vue:18 +msgid "Visibility" +msgstr "Visibilidade" #: front/src/components/playlists/PlaylistModal.vue:20 msgid "We cannot add the track to a playlist" -msgstr "" +msgstr "Não podemos adicionar a música a uma playlist" #: front/src/components/playlists/Form.vue:14 msgid "We cannot create the playlist" -msgstr "" +msgstr "Não podemos criar a playlist" #: front/src/components/auth/Signup.vue:13 msgid "We cannot create your account" -msgstr "" +msgstr "Não podemos criar sua conta" #: front/src/components/auth/Login.vue:7 msgid "We cannot log you in" -msgstr "" +msgstr "Nós não podemos te logar" #: front/src/components/auth/Settings.vue:38 msgid "We cannot save your avatar" -msgstr "" +msgstr "Não podemos salvar seu avatar" #: front/src/components/auth/Settings.vue:14 msgid "We cannot save your settings" -msgstr "" +msgstr "Não podemos salvar suas configurações" -#: front/src/components/Home.vue:130 +#: front/src/components/Home.vue:127 msgid "We do not track you or bother you with ads" -msgstr "" +msgstr "Nós não rastreamos ou incomodamos você com anúncios" -#: front/src/components/library/import/FileUpload.vue:5 -msgid "We recommend using Picard for that purpose." +#: front/src/views/Notifications.vue:26 +msgid "We don't have any notification to display!" +msgstr "Não temos nenhuma notificação para exibir!" + +#: front/src/views/content/Home.vue:4 +msgid "We offer various way to grab new content and make it available here." msgstr "" +"Oferecemos várias maneiras de pegar novos conteúdos e disponibilizá-los aqui." + +#: front/src/components/library/FileUpload.vue:40 +msgid "we recommend using Picard for that purpose" +msgstr "recomendamos o uso do Picard para esse fim" #: front/src/components/Home.vue:7 msgid "We think listening to music should be simple." -msgstr "" +msgstr "Nós achamos que ouvir música deveria ser simples." #: front/src/components/PageNotFound.vue:10 msgid "We're sorry, the page you asked for does not exist:" -msgstr "" +msgstr "Desculpe, a página que você pediu não existe:" #: front/src/components/requests/Form.vue:21 msgid "We've received your request, you'll get some groove soon ;)" msgstr "" -#: front/src/components/Home.vue:152 -msgid "Welcome" -msgstr "" - #: front/src/components/Home.vue:5 msgid "Welcome on Funkwhale" -msgstr "" - -#: front/src/components/library/import/Main.vue:114 -msgid "What is metadata?" -msgstr "" - -#: front/src/views/federation/LibraryDetail.vue:197 -msgid "When enabled, auto importing will automatically import new tracks published in this library" -msgstr "" +msgstr "Bem-vindo ao Funkwhale" #: front/src/components/Home.vue:24 msgid "Why funkwhale?" -msgstr "" +msgstr "Por que o funkwhale?" -#: front/src/components/Sidebar.vue:124 +#: front/src/components/Sidebar.vue:115 msgid "Yes" -msgstr "" +msgstr "Sim" #: front/src/components/auth/Logout.vue:8 msgid "Yes, log me out!" +msgstr "Sim, me desconecte!" + +#: front/src/components/library/FileUpload.vue:33 +msgid "You are about to upload music to your library. Before proceeding, please ensure that:" msgstr "" +"Você está prestes a fazer o upload de músicas para sua biblioteca. Antes de " +"prosseguir, certifique-se de que:" #: front/src/components/auth/Logout.vue:7 msgid "You are currently logged in as %{ username }" -msgstr "" +msgstr "Você está logado como %{ username }" -#: front/src/components/library/import/Main.vue:111 -msgid "You can also skip this step and enter metadata manually." +#: front/src/views/content/Home.vue:17 +msgid "You can follow libraries from other users to get access to new music. Public libraries can be followed immediatly, while following a private library requires approval from its owner." msgstr "" +"Você pode seguir bibliotecas de outros usuários para obter acesso a novas " +"músicas. Bibliotecas públicas podem ser seguidas imediatamente, enquanto " +"seguir uma biblioteca privada requer aprovação de seu proprietário." -#: front/src/components/Home.vue:136 +#: front/src/components/Home.vue:133 msgid "You can invite friends and family to your instance so they can enjoy your music" msgstr "" +"Você pode convidar amigos e familia para sua instância para que eles possam " +"curtir sua música" #: front/src/components/library/radios/Builder.vue:7 msgid "You can use this interface to build your own custom radio, which will play tracks according to your criteria." msgstr "" +"Você pode usar essa interface para construir a sua própria rádio que vai " +"jogar músicas de acordo com seus critérios." #: front/src/components/auth/SubsonicTokenForm.vue:8 msgid "You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance." msgstr "" +"Você pode usá-los para curtir sua playlist e música no modo off-line, em seu " +"smartphone ou tablet, por exemplo." -#: front/src/components/Sidebar.vue:156 +#: front/src/components/Sidebar.vue:147 msgid "You have a radio playing" -msgstr "" +msgstr "Você tem um rádio tocando" -#: front/src/App.vue:6 +#: front/src/App.vue:8 msgid "You need to select an instance in order to continue" +msgstr "Você precisa selecionar uma instância para continuar" + +#: front/src/views/content/libraries/Form.vue:19 +msgid "You will be able to share your library with other people, regardless of it's visibility." msgstr "" +"Você poderá compartilhar sua biblioteca com outras pessoas, " +"independentemente de sua visibilidade." #: front/src/components/auth/Settings.vue:100 msgid "You will be logged out from this session and have to log in with the new one" -msgstr "" +msgstr "Você será desconectado desta sessão e precisará fazer login com o novo" #: front/src/components/auth/Settings.vue:71 msgid "You will have to update your password on your clients that use this password." -msgstr "" - -#: front/src/components/library/import/Main.vue:103 -msgid "You will import:" -msgstr "" +msgstr "Você terá que atualizar sua senha em seus clientes que usam essa senha." #: front/src/views/auth/EmailConfirm.vue:24 msgid "Your email address was confirmed, you can now use the service without limitations." msgstr "" +"Seu endereço de e-mail foi confirmado, agora você pode usar o serviço sem " +"limitações." -#: front/src/components/favorites/List.vue:109 -msgid "Your Favorites" -msgstr "" - -#: front/src/components/Home.vue:117 +#: front/src/components/Home.vue:114 msgid "Your music, your way" -msgstr "" +msgstr "Sua música, seu jeito" + +#: front/src/views/Notifications.vue:7 +msgid "Your notifications" +msgstr "Suas notificações" #: front/src/views/auth/PasswordResetConfirm.vue:29 msgid "Your password has been updated successfully." -msgstr "" +msgstr "Sua senha foi atualizada com sucesso." #: front/src/components/auth/Settings.vue:101 msgid "Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password" msgstr "" +"Sua senha do Subsonic será alterada para uma nova, aleatória, efetuando o " +"logout de dispositivos que usaram a senha antiga do Subsonic" -#: front/src/components/audio/PlayButton.vue:156 +#: front/src/components/mixins/Translations.vue:8 +msgid "Activity visibility" +msgstr "Visibilidade da atividade" + +#: front/src/components/mixins/Translations.vue:9 +msgid "Determine the visibility level of your activity" +msgstr "Determinar o nível de visibilidade de sua atividade" + +#: front/src/components/mixins/Translations.vue:11 +#: front/src/components/playlists/Form.vue:81 +#: src/views/content/libraries/Form.vue:72 +msgid "Nobody except me" +msgstr "Ninguém, exceto eu" + +#: front/src/components/mixins/Translations.vue:12 +#: front/src/components/playlists/Form.vue:85 +#: src/views/content/libraries/Form.vue:73 +msgid "Everyone on this instance" +msgstr "Todos nessa instância" + +#: front/src/components/mixins/Translations.vue:18 +msgid "Accessed date" +msgstr "Data de acesso" + +#: front/src/components/mixins/Translations.vue:19 +msgid "Modification date" +msgstr "Data de modificação" + +#: front/src/components/mixins/Translations.vue:20 +msgid "Imported date" +msgstr "Data de importação" + +#: front/src/components/mixins/Translations.vue:22 +msgid "Track name" +msgstr "Título da música" + +#: front/src/components/mixins/Translations.vue:23 +msgid "Album name" +msgstr "Nome do álbum" + +#: front/src/components/mixins/Translations.vue:30 +msgid "Sign-up date" +msgstr "Data de inscrição" + +#: front/src/components/playlists/Editor.vue:163 +msgid "Copy tracks from current queue to playlist" +msgstr "Copiar músicas da fila atual para a playlist" + +#: front/src/components/playlists/PlaylistModal.vue:116 +msgid "Add to this playlist" +msgstr "Adicionar a esta playlist" + +#: front/src/components/playlists/Form.vue:74 +msgid "My awesome playlist" +msgstr "Minha playlist incrível" + +#: front/src/components/playlists/Form.vue:89 +msgid "Everyone" +msgstr "Todo o mundo" + +#: front/src/components/auth/Signup.vue:95 +msgid "Sign Up" +msgstr "Inscrever-se" + +#: front/src/components/auth/Signup.vue:96 +msgid "Enter your invitation code (case insensitive)" +msgstr "Indicar seu código de convite (não diferencia maiúsculas de minúsculas)" + +#: front/src/components/auth/Signup.vue:97 +msgid "Enter your username" +msgstr "Indicar seu nome de usuário" + +#: front/src/components/auth/Signup.vue:98 +msgid "Enter your email" +msgstr "Insira seu email" + +#: front/src/components/auth/SubsonicTokenForm.vue:95 +msgid "Password updated" +msgstr "Senha atualizada" + +#: front/src/components/auth/SubsonicTokenForm.vue:111 +msgid "Access disabled" +msgstr "Acesso desativado" + +#: front/src/components/auth/Login.vue:77 +msgid "Enter your username or email" +msgstr "Indicar seu nome de usuário ou email" + +#: front/src/components/auth/Login.vue:78 +msgid "Log In" +msgstr "Autenticação" + +#: front/src/components/auth/Profile.vue:47 +msgid "%{ username }'s profile" +msgstr "%{ username } perfil" + +#: front/src/components/auth/Logout.vue:20 +msgid "Log Out" +msgstr "Sair" + +#: front/src/components/auth/Settings.vue:249 +msgid "Account Settings" +msgstr "Configurações da conta" + +#: front/src/components/favorites/TrackFavoriteIcon.vue:19 +msgid "Remove from favorites" +msgstr "Remover dos favoritos" + +#: front/src/components/favorites/List.vue:110 +msgid "Your Favorites" +msgstr "Seus favoritos" + +#: front/src/components/library/Radios.vue:141 +msgid "Enter a radio name..." +msgstr "Indicar um nome de rádio ..." + +#: front/src/components/library/radios/Builder.vue:233 +msgid "Radio Builder" +msgstr "Construtor de rádio" + +#: front/src/components/library/radios/Builder.vue:235 +msgid "My awesome radio" +msgstr "Meu rádio incrível" + +#: front/src/components/library/radios/Builder.vue:236 +msgid "My awesome description" +msgstr "Minha descrição incrível" + +#: front/src/components/library/FileUpload.vue:238 +msgid "Upload refused, ensure the file is not too big and you have not reached your quota" +msgstr "" +"Upload recusado, verifique se o ficheiro não é muito grande e se você não " +"atingiu sua quota" + +#: front/src/components/library/FileUpload.vue:239 +msgid "Impossible to upload this file, ensure it is not too big" +msgstr "Impossível carregar este ficheiro, garantir que não é muito grande" + +#: front/src/components/library/FileUpload.vue:240 +msgid "A network error occured while uploading this file" +msgstr "Ocorreu um erro durante o upload deste ficheiro" + +#: front/src/components/library/FileUpload.vue:241 +msgid "Upload timeout, please try again" +msgstr "Tempo limite de upload, por favor, tente novamente" + +#: front/src/components/library/Artists.vue:119 +msgid "Enter an artist name..." +msgstr "Indicar um nome de artista ..." + +#: front/src/components/library/Track.vue:195 +#: src/components/audio/SearchBar.vue:27 +#: front/src/components/metadata/Search.vue:138 +msgid "Track" +msgstr "Música" + +#: front/src/components/library/Home.vue:65 +msgid "Home" +msgstr "Casa" + +#: front/src/components/forms/PasswordInput.vue:25 +msgid "Show/hide password" +msgstr "Mostrar/ocultar senha" + +#: front/src/components/requests/Form.vue:73 +msgid "The Beatles, Mickael Jackson…" +msgstr "The Beatles, Mickael Jackson…" + +#: front/src/components/requests/Form.vue:74 +msgid "The White Album, Thriller…" +msgstr "The White Album, Thriller…" + +#: front/src/components/requests/Form.vue:75 +msgid "Use this comment box to add details to your request if needed" +msgstr "" +"Use esta caixa de comentários para adicionar detalhes à sua solicitação, se " +"necessário" + +#: front/src/components/audio/PlayButton.vue:158 msgid "%{ count } track was added to your queue" msgid_plural "%{ count } tracks were added to your queue" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%{ count } música foi adicionada à sua fila" +msgstr[1] "%{ count } músicas foram adicionadas à sua fila" + +#: front/src/components/audio/Search.vue:65 +msgid "Artist, album, track..." +msgstr "Artista, álbum, música ..." + +#: front/src/components/audio/SearchBar.vue:20 +msgid "Search for artists, albums, tracks..." +msgstr "Procurar artistas, álbuns, músicas..." + +#: front/src/components/audio/Player.vue:216 +msgid "Queue shuffled!" +msgstr "" + +#: front/src/components/audio/Player.vue:273 +msgid "Previous track" +msgstr "Música anterior" + +#: front/src/components/audio/Player.vue:274 +msgid "Play track" +msgstr "Jogar mũsica" + +#: front/src/components/audio/Player.vue:275 +msgid "Pause track" +msgstr "Parar música" + +#: front/src/components/audio/Player.vue:276 +msgid "Next track" +msgstr "Música seguinte" + +#: front/src/components/audio/Player.vue:277 +msgid "Unmute" +msgstr "Cancelar mudo" + +#: front/src/components/audio/Player.vue:278 +msgid "Mute" +msgstr "Mudo" + +#: front/src/components/audio/Player.vue:279 +msgid "Looping disabled. Click to switch to single-track looping." +msgstr "Looping desativado. Clique para alternar para um loop de musica única." + +#: front/src/components/audio/Player.vue:280 +msgid "Looping on a single track. Click to switch to whole queue looping." +msgstr "Looping em uma única música. Clique para alternar para todo o loop da fila." + +#: front/src/components/audio/Player.vue:281 +msgid "Looping on whole queue. Click to disable looping." +msgstr "Looping na fila inteira. Clique para desativar o loop." + +#: front/src/components/audio/Player.vue:282 +msgid "Shuffle your queue" +msgstr "" + +#: front/src/components/audio/Player.vue:283 +msgid "Clear your queue" +msgstr "Limpar sua fila" + +#: front/src/components/Sidebar.vue:203 +msgid "Pending import requests" +msgstr "Pedidos de importação pendentes" + +#: front/src/components/Sidebar.vue:204 +msgid "Pending follow requests" +msgstr "" + +#: front/src/components/metadata/Search.vue:114 +msgid "Enter your search query..." +msgstr "Insira sua consulta de pesquisa ..." + +#: front/src/components/manage/library/FilesTable.vue:176 +msgid "Search by title, artist, domain..." +msgstr "Pesquisa por título, artista, domínio..." + +#: front/src/components/manage/users/InvitationForm.vue:58 +msgid "Leave empty for a random code" +msgstr "Deixar vazio para um código aleatório" + +#: front/src/components/manage/users/InvitationsTable.vue:153 +msgid "Search by username, email, code..." +msgstr "Pesquisa por nome de usuário, e-mail, código ..." + +#: front/src/components/manage/users/UsersTable.vue:163 +msgid "Search by username, email, name..." +msgstr "Pesquisa por nome de usuário, e-mail, nome..." + +#: front/src/components/manage/users/UsersTable.vue:181 +#: front/src/views/admin/users/UsersDetail.vue:185 +#: src/views/admin/Settings.vue:83 +msgid "Federation" +msgstr "Federação" + +#: front/src/components/Home.vue:154 +msgid "Welcome" +msgstr "Bem-vindo" + +#: front/src/views/content/remote/ScanForm.vue:48 +msgid "Enter a library url" +msgstr "Insira um URL de biblioteca" + +#: front/src/views/content/remote/Card.vue:165 +msgid "Scan launched" +msgstr "Análise iniciada" + +#: front/src/views/content/remote/Card.vue:166 +msgid "Scan skipped (previous scan is too recent)" +msgstr "Análise ignorada (a análise anterior é muito recente)" + +#: front/src/views/content/libraries/FilesTable.vue:226 +msgid "Search by title, artist, album..." +msgstr "Pesquisa por título, artista, álbum ..." + +#: front/src/views/content/libraries/FilesTable.vue:230 +msgid "Track was already present in one of your libraries" +msgstr "A música já estava presente em uma de suas bibliotecas" + +#: front/src/views/content/libraries/FilesTable.vue:234 +msgid "Track is uploaded but not processed by the server yet" +msgstr "Música é carregada, mas ainda não é processada pelo servidor" + +#: front/src/views/content/libraries/FilesTable.vue:238 +msgid "An error occured while processing this track, ensure the track is correctly tagged" +msgstr "" +"Ocorreu um erro durante o processamento desta música, certifique-se de que a " +"música está corretamente etiquetada" + +#: front/src/views/content/libraries/FilesTable.vue:242 +msgid "Import went on successfully" +msgstr "A importação foi bem sucedida" + +#: front/src/views/content/libraries/FilesTable.vue:259 +msgid "Relaunch import" +msgstr "Reinicie a importação" + +#: front/src/views/content/libraries/Card.vue:58 +msgid "Visibility: nobody except me" +msgstr "Visibilidade: ninguém exceto eu" + +#: front/src/views/content/libraries/Card.vue:59 +msgid "Visibility: everyone on this instance" +msgstr "Visibilidade: todos nesta instância" + +#: front/src/views/content/libraries/Card.vue:60 +msgid "Visibility: everyone, including other instances" +msgstr "Visibilidade: todos, incluindo outras instâncias" + +#: front/src/views/content/libraries/Card.vue:61 +msgid "Total size of the files in this library" +msgstr "Tamanho total dos arquivos nesta biblioteca" + +#: front/src/views/content/libraries/Form.vue:70 +msgid "My awesome library" +msgstr "Minha biblioteca incrível" + +#: front/src/views/content/libraries/Form.vue:71 +msgid "This library contains my personnal music, I hope you will like it!" +msgstr "Esta biblioteca contém minha música pessoal, espero que você goste!" + +#: front/src/views/content/libraries/Form.vue:74 +msgid "Everyone, including other instances" +msgstr "Todos, incluindo outras instâncias" + +#: front/src/views/content/libraries/Form.vue:106 +msgid "Library updated" +msgstr "Biblioteca atualizada" + +#: front/src/views/content/libraries/Form.vue:109 +msgid "Library created" +msgstr "Biblioteca criada" + +#: front/src/views/content/Home.vue:35 +msgid "Add and manage content" +msgstr "Adicionar e gerenciar conteúdo" + +#: front/src/views/radios/Detail.vue:80 +msgid "Radio" +msgstr "Rádio" + +#: front/src/views/instance/Timeline.vue:57 +msgid "Instance Timeline" +msgstr "Linha do tempo da instância" + +#: front/src/views/playlists/Detail.vue:90 +msgid "Playlist" +msgstr "Playlist" + +#: front/src/views/playlists/List.vue:105 +msgid "Enter an playlist name..." +msgstr "Indicar um nome de playlist..." + +#: front/src/views/admin/library/Base.vue:16 +msgid "Manage library" +msgstr "Gerenciar biblioteca" + +#: front/src/views/admin/users/UsersDetail.vue:169 +msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." +msgstr "Determine se a conta do usuário está ativa ou não. Usuários inativos não podem fazer autenticação ou usar o serviço." + +#: front/src/views/admin/users/UsersDetail.vue:170 +msgid "Determine how much content the user can upload. Leave empty to use the default value of the instance." +msgstr "" +"Determine quanto conteúdo o usuário pode enviar. Deixe em branco para usar o " +"valor padrão da instância." + +#: front/src/views/admin/users/Base.vue:20 +msgid "Manage users" +msgstr "Gerenciar usuários" + +#: front/src/views/admin/Settings.vue:75 +msgid "Instance settings" +msgstr "Configurações da instância" + +#: front/src/views/admin/Settings.vue:80 +msgid "Instance information" +msgstr "Informação da instância" + +#: front/src/views/admin/Settings.vue:84 +msgid "Subsonic" +msgstr "" + +#: front/src/views/admin/Settings.vue:85 +msgid "Statistics" +msgstr "Estatisticas" + +#: front/src/views/admin/Settings.vue:86 +msgid "Error reporting" +msgstr "Relatório de erros" diff --git a/front/locales/sv/LC_MESSAGES/app.po b/front/locales/sv/LC_MESSAGES/app.po index 60bd0e9d2..d9b32b01f 100644 --- a/front/locales/sv/LC_MESSAGES/app.po +++ b/front/locales/sv/LC_MESSAGES/app.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: front 1.0.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-07-17 19:29+0200\n" -"PO-Revision-Date: 2018-07-23 16:49+0000\n" +"PO-Revision-Date: 2018-10-03 16:06+0000\n" "Last-Translator: Tim Stahel \n" "Language-Team: none\n" "Language: sv\n" @@ -272,7 +272,7 @@ msgstr "" #: front/src/components/library/import/BatchList.vue:13 #: front/src/components/library/import/BatchList.vue:22 msgid "Any" -msgstr "" +msgstr "Vilken som helst" #: front/src/components/library/import/BatchList.vue:24 msgid "API" @@ -377,27 +377,27 @@ msgstr "Bläddra artister" #: front/src/views/federation/LibraryTrackList.vue:3 msgid "Browsing federated tracks" -msgstr "" +msgstr "Bläddrar genom federerade spår" #: front/src/views/federation/LibraryFollowersList.vue:3 msgid "Browsing followers" -msgstr "" +msgstr "Bläddrar genom följare" #: front/src/views/federation/LibraryList.vue:3 msgid "Browsing libraries" -msgstr "" +msgstr "Bläddrar genom bibliotek" #: front/src/views/playlists/List.vue:3 msgid "Browsing playlists" -msgstr "" +msgstr "Bläddrar genom spellistor" #: front/src/components/library/Radios.vue:4 msgid "Browsing radios" -msgstr "" +msgstr "Bläddrar genom radiostationer" #: front/src/components/library/radios/Builder.vue:5 msgid "Builder" -msgstr "" +msgstr "Byggare" #: front/src/components/audio/album/Card.vue:13 msgid "By %{ artist }" @@ -456,6 +456,8 @@ msgstr "Ändringar synkade med servern" #: front/src/components/auth/Settings.vue:70 msgid "Changing your password will also change your Subsonic API password if you have requested one." msgstr "" +"Ändring av lösenord kommer också att ändra ditt lösenord för Subsonic API:n " +"om du har efterfrågat ett." #: front/src/components/auth/Settings.vue:98 msgid "Changing your password will have the following consequences" @@ -467,141 +469,141 @@ msgstr "Välj din instans" #: front/src/components/Home.vue:64 msgid "Clean library" -msgstr "" +msgstr "Rensa bibliotek" #: front/src/components/manage/users/InvitationForm.vue:37 msgid "Clear" -msgstr "" +msgstr "Rensa" #: front/src/components/playlists/Editor.vue:40 #: front/src/components/playlists/Editor.vue:45 msgid "Clear playlist" -msgstr "" +msgstr "Rensa spellista" #: front/src/components/audio/Player.vue:270 msgid "Clear your queue" -msgstr "" +msgstr "Rensa din kö" #: front/src/components/library/import/BatchList.vue:23 msgid "CLI" -msgstr "" +msgstr "Kommandolinje" #: front/src/components/Home.vue:44 msgid "Click once, listen for hours using built-in radios" -msgstr "" +msgstr "Klicka en gång, lyssna i flera timmarmed inbyggda radiostationer" #: front/src/components/manage/library/RequestsTable.vue:30 #: front/src/components/manage/library/RequestsTable.vue:64 msgid "Closed" -msgstr "" +msgstr "Stängd" #: front/src/components/manage/users/InvitationForm.vue:26 #: front/src/components/manage/users/InvitationsTable.vue:42 msgid "Code" -msgstr "" +msgstr "Kod" #: front/src/components/audio/album/Card.vue:43 #: front/src/components/audio/artist/Card.vue:33 #: front/src/components/discussion/Comment.vue:20 msgid "Collapse" -msgstr "" +msgstr "Kollapsa" #: front/src/components/manage/library/RequestsTable.vue:51 #: front/src/components/requests/Form.vue:14 msgid "Comment" -msgstr "" +msgstr "Kommentera" #: front/src/components/library/radios/Builder.vue:48 msgid "Config" -msgstr "" +msgstr "Konfiguration" #: front/src/components/common/DangerousButton.vue:21 msgid "Confirm" -msgstr "" +msgstr "Bekräfta" #: front/src/views/auth/EmailConfirm.vue:4 src/views/auth/EmailConfirm.vue:20 #: front/src/views/auth/EmailConfirm.vue:51 msgid "Confirm your email" -msgstr "" +msgstr "Bekräfta din e-post" #: front/src/views/auth/EmailConfirm.vue:13 msgid "Confirmation code" -msgstr "" +msgstr "Bekräftningskod" #: front/src/components/playlists/Editor.vue:163 msgid "Copy tracks from current queue to playlist" -msgstr "" +msgstr "Kopiera spår från nuvarande kö till spellista" #: front/src/components/Home.vue:88 msgid "Covers, lyrics, our goal is to have them all ;)" -msgstr "" +msgstr "Omslag, text, vårat mål är att ha dem alla ;)" #: front/src/components/auth/Signup.vue:4 msgid "Create a funkwhale account" -msgstr "" +msgstr "Skapa ett funkwhale-konto" #: front/src/components/playlists/Form.vue:2 msgid "Create a new playlist" -msgstr "" +msgstr "Skapa en ny spellista" #: front/src/components/auth/Login.vue:17 msgid "Create an account" -msgstr "" +msgstr "Skapa ett konto" #: front/src/components/manage/library/RequestsTable.vue:88 #: front/src/components/requests/Card.vue:25 msgid "Create import" -msgstr "" +msgstr "Skapa import" #: front/src/components/auth/Signup.vue:51 msgid "Create my account" -msgstr "" +msgstr "Skapa mitt konto" #: front/src/components/playlists/Form.vue:34 msgid "Create playlist" -msgstr "" +msgstr "Skapa spellista" #: front/src/components/library/Radios.vue:23 msgid "Create your own radio" -msgstr "" +msgstr "Skapa din egen radiostation" #: front/src/components/federation/LibraryFollowTable.vue:22 #: front/src/components/manage/library/RequestsTable.vue:52 #: front/src/components/manage/users/InvitationsTable.vue:40 msgid "Creation date" -msgstr "" +msgstr "Skapningsdatum" #: front/src/components/auth/Settings.vue:54 msgid "Current avatar" -msgstr "" +msgstr "Nuvarande avatar" #: front/src/components/playlists/PlaylistModal.vue:8 msgid "Current track" -msgstr "" +msgstr "Nuvarande spår" #: front/src/components/manage/library/FilesTable.vue:189 #: front/src/components/manage/library/RequestsTable.vue:195 #: front/src/components/manage/users/InvitationsTable.vue:166 #: front/src/views/playlists/Detail.vue:33 msgid "Delete" -msgstr "" +msgstr "Radera" #: front/src/views/playlists/Detail.vue:38 msgid "Delete playlist" -msgstr "" +msgstr "Radera spellista" #: front/src/views/radios/Detail.vue:28 msgid "Delete radio" -msgstr "" +msgstr "Radera radiostation" #: front/src/components/federation/LibraryFollowTable.vue:52 #: front/src/components/federation/LibraryFollowTable.vue:63 msgid "Deny" -msgstr "" +msgstr "Neka" #: front/src/components/federation/LibraryFollowTable.vue:54 msgid "Deny access?" -msgstr "" +msgstr "Neka åtkomst?" #: front/src/components/favorites/List.vue:34 #: front/src/components/federation/LibraryTrackTable.vue:29 @@ -611,108 +613,110 @@ msgstr "" #: front/src/components/manage/users/UsersTable.vue:20 #: front/src/views/federation/LibraryList.vue:29 src/views/playlists/List.vue:28 msgid "Descending" -msgstr "" +msgstr "Sjunkande" #: front/src/components/federation/LibraryCard.vue:50 msgid "Detail" -msgstr "" +msgstr "Detalj" #: front/src/views/admin/users/UsersDetail.vue:146 msgid "Determine if the user account is active or not. Inactive users cannot login or use the service." msgstr "" +"Avgör om användarkontot är aktivt eller inte. Inaktiva användare kan inte " +"logga in eller använda tjänsten." #: front/src/components/auth/Settings.vue:104 #: front/src/components/auth/SubsonicTokenForm.vue:52 msgid "Disable access" -msgstr "" +msgstr "Inaktivera åtkomst" #: front/src/components/auth/SubsonicTokenForm.vue:49 msgid "Disable Subsonic access" -msgstr "" +msgstr "Inaktivera Subsonic-åtkomst" #: front/src/components/auth/SubsonicTokenForm.vue:50 msgid "Disable Subsonic API access?" -msgstr "" +msgstr "Inaktivera åtkomst till Subsonic-API:n?" #: front/src/components/auth/SubsonicTokenForm.vue:14 msgid "Discover how to use Funkwhale from other apps" -msgstr "" +msgstr "Upptäck hur du kan använda Funkwhale från andra appar" #: front/src/components/library/radios/Builder.vue:16 msgid "Display publicly" -msgstr "" +msgstr "Visa publikt" #: front/src/components/playlists/Editor.vue:42 msgid "Do you want to clear the playlist \"%{ playlist }\"?" -msgstr "" +msgstr "Vill du rensa spellistan \"%{ playlist }\"?" #: front/src/components/common/DangerousButton.vue:7 msgid "Do you want to confirm this action?" -msgstr "" +msgstr "Vill du bekräfta denna handling?" #: front/src/views/playlists/Detail.vue:35 msgid "Do you want to delete the playlist \"%{ playlist }\"?" -msgstr "" +msgstr "Vill du radera spellistan \"%{ playlist }\"?" #: front/src/views/radios/Detail.vue:26 msgid "Do you want to delete the radio \"{{ radio }}\"?" -msgstr "" +msgstr "Vill du radera radiokanalen \"{{ radio }}\"?" #: front/src/components/common/ActionTable.vue:29 msgid "Do you want to launch %{ action } on %{ count } element?" msgid_plural "Do you want to launch %{ action } on %{ count } elements?" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Vill du starta %{ action } på %{ count } element?" +msgstr[1] "Vill du starta %{ action } på %{ count } element?" #: front/src/components/Sidebar.vue:113 msgid "Do you want to restore your previous queue?" -msgstr "" +msgstr "Vill du återställa din föregående kö?" #: front/src/App.vue:38 msgid "Documentation" -msgstr "" +msgstr "Dokumentation" #: front/src/components/audio/track/Table.vue:24 src/components/library/Track.vue:48 msgid "Download" -msgstr "" +msgstr "Ladda ner" #: front/src/components/audio/track/Table.vue:27 msgid "Download tracks" -msgstr "" +msgstr "Ladda ner spår" #: front/src/components/playlists/Editor.vue:49 msgid "Drag and drop rows to reorder tracks in the playlist" -msgstr "" +msgstr "Dra och släpp rader för att omordna spår i spellistan" #: front/src/components/library/Track.vue:58 #: front/src/components/manage/library/FilesTable.vue:43 msgid "Duration" -msgstr "" +msgstr "Tid" #: front/src/components/Home.vue:96 msgid "Easy to use" -msgstr "" +msgstr "Enkelt att använda" #: front/src/components/About.vue:21 msgid "Edit instance info" -msgstr "" +msgstr "Redigera info om instans" #: front/src/components/radios/Card.vue:20 src/views/playlists/Detail.vue:30 msgid "Edit..." -msgstr "" +msgstr "Redigera..." #: front/src/components/auth/Signup.vue:29 #: front/src/components/manage/users/UsersTable.vue:38 msgid "Email" -msgstr "" +msgstr "E-post" #: front/src/views/admin/users/UsersDetail.vue:29 msgid "Email address" -msgstr "" +msgstr "E-postadress" #: front/src/views/auth/EmailConfirm.vue:23 msgid "Email confirmed" -msgstr "" +msgstr "E-post bekräftad" #: front/src/views/playlists/Detail.vue:29 msgid "End edition" @@ -724,11 +728,11 @@ msgstr "" #: front/src/components/library/Radios.vue:140 msgid "Enter a radio name..." -msgstr "" +msgstr "Ange namn på radiostation..." #: front/src/components/library/Artists.vue:118 msgid "Enter an artist name..." -msgstr "" +msgstr "Ange ett artistnamn..." #: front/src/views/federation/LibraryList.vue:122 msgid "Enter an library domain name..." @@ -736,39 +740,39 @@ msgstr "" #: front/src/views/playlists/List.vue:104 msgid "Enter an playlist name..." -msgstr "" +msgstr "Ange ett spellistenamn..." #: front/src/components/auth/Signup.vue:98 msgid "Enter your email" -msgstr "" +msgstr "Ange din e-post" #: front/src/components/auth/Signup.vue:96 msgid "Enter your invitation code (case insensitive)" -msgstr "" +msgstr "Ange din inbjudningskod (skiftlägesokänsligt)" #: front/src/components/metadata/Search.vue:114 msgid "Enter your search query..." -msgstr "" +msgstr "Ange dina söktermer..." #: front/src/components/auth/Signup.vue:97 msgid "Enter your username" -msgstr "" +msgstr "Ange ditt användarnamn" #: front/src/components/auth/Login.vue:77 msgid "Enter your username or email" -msgstr "" +msgstr "Ange ditt användarnamn eller e-post" #: front/src/components/auth/SubsonicTokenForm.vue:20 msgid "Error" -msgstr "" +msgstr "Fel" #: front/src/views/admin/Settings.vue:87 msgid "Error reporting" -msgstr "" +msgstr "Felrapportering" #: front/src/components/common/ActionTable.vue:75 msgid "Error while applying action" -msgstr "" +msgstr "Fel vid handling" #: front/src/views/auth/PasswordReset.vue:7 msgid "Error while asking for a password reset" diff --git a/front/scripts/i18n-compile.sh b/front/scripts/i18n-compile.sh index 211f8ee8c..955913393 100755 --- a/front/scripts/i18n-compile.sh +++ b/front/scripts/i18n-compile.sh @@ -1,3 +1,3 @@ #!/bin/bash -eux locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | xargs echo) -find locales -name '*.po' | xargs $(yarn bin gettext-extract)/gettext-compile --output src/translations.json +find locales -name '*.po' | xargs $(yarn bin)/gettext-compile --output src/translations.json diff --git a/front/scripts/i18n-extract.sh b/front/scripts/i18n-extract.sh index 149d3262d..1b36d7b1c 100755 --- a/front/scripts/i18n-extract.sh +++ b/front/scripts/i18n-extract.sh @@ -7,7 +7,7 @@ touch $locales_dir/app.pot # Create a main .pot template, then generate .po files for each available language. # Extract gettext strings from templates files and create a POT dictionary template. -$(yarn bin gettext-extract)/gettext-extract --attribute v-translate --quiet --output $locales_dir/app.pot $sources +$(yarn bin)/gettext-extract --attribute v-translate --quiet --output $locales_dir/app.pot $sources xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \ --from-code=utf-8 --join-existing --no-wrap \ --package-name=$(node -e "console.log(require('./package.json').name);") \ diff --git a/front/src/App.vue b/front/src/App.vue index 0bfdeb8b2..16154b130 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -34,9 +34,6 @@ About this instance - - Request music - Official website Documentation @@ -94,6 +91,8 @@ import axios from 'axios' import _ from 'lodash' import {mapState} from 'vuex' +import { WebSocketBridge } from 'django-channels' + import translations from '@/translations' @@ -113,11 +112,13 @@ export default { }, data () { return { + bridge: null, nodeinfo: null, instanceUrl: null } }, created () { + this.openWebsocket() let self = this this.autodetectLanguage() setInterval(() => { @@ -134,8 +135,23 @@ export default { this.$store.dispatch('auth/check') this.$store.dispatch('instance/fetchSettings') this.fetchNodeInfo() + this.$store.commit('ui/addWebsocketEventHandler', { + eventName: 'inbox.item_added', + id: 'sidebarCount', + handler: this.incrementNotificationCountInSidebar + }) + }, + destroyed () { + this.$store.commit('ui/removeWebsocketEventHandler', { + eventName: 'inbox.item_added', + id: 'sidebarCount', + }) + this.disconnect() }, methods: { + incrementNotificationCountInSidebar (event) { + this.$store.commit('ui/incrementNotifications', {type: 'inbox', count: 1}) + }, fetchNodeInfo () { let self = this axios.get('instance/nodeinfo/2.0/').then(response => { @@ -162,6 +178,36 @@ export default { } else if (almostMatching.length > 0) { this.$language.current = almostMatching[0] } + }, + disconnect () { + if (!this.bridge) { + return + } + this.bridge.socket.close(1000, 'goodbye', {keepClosed: true}) + }, + openWebsocket () { + if (!this.$store.state.auth.authenticated) { + return + } + this.disconnect() + let self = this + let token = this.$store.state.auth.token + // let token = 'test' + const bridge = new WebSocketBridge() + this.bridge = bridge + let url = this.$store.getters['instance/absoluteUrl'](`api/v1/activity?token=${token}`) + url = url.replace('http://', 'ws://') + url = url.replace('https://', 'wss://') + bridge.connect( + url, + null, + {reconnectInterval: 5000}) + bridge.listen(function (event) { + self.$store.dispatch('ui/websocketEvent', event) + }) + bridge.socket.addEventListener('open', function () { + console.log('Connected to WebSocket') + }) } }, computed: { @@ -189,6 +235,13 @@ export default { this.$store.dispatch('instance/fetchSettings') this.fetchNodeInfo() }, + '$store.state.auth.authenticated' (newValue) { + if (!newValue) { + this.disconnect() + } else { + this.openWebsocket() + } + }, '$language.current' (newValue) { this.$store.commit('ui/currentLanguage', newValue) } @@ -250,7 +303,7 @@ html, body { left: 350px; right: 0px; top: 0px; - z-index: 2000; + z-index: 99; } background-color: white; .item { @@ -299,9 +352,11 @@ html, body { } } -.discrete.link { - color: rgba(0, 0, 0, 0.87); - cursor: pointer; +.discrete { + color: rgba(0, 0, 0, 0.87); +} +.link { + cursor: pointer; } .floated.buttons .button ~ .dropdown { @@ -318,4 +373,11 @@ html, body { margin: 0.5em; } +a { + cursor: pointer; +} +.segment.hidden { + display: none; +} + diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue index 648b1afc7..3448d10df 100644 --- a/front/src/components/Home.vue +++ b/front/src/components/Home.vue @@ -74,13 +74,10 @@
-
@@ -146,6 +143,11 @@ diff --git a/front/src/components/common/ActorLink.vue b/front/src/components/common/ActorLink.vue new file mode 100644 index 000000000..518e67c86 --- /dev/null +++ b/front/src/components/common/ActorLink.vue @@ -0,0 +1,17 @@ + + + diff --git a/front/src/components/common/CopyInput.vue b/front/src/components/common/CopyInput.vue new file mode 100644 index 000000000..af82f2c66 --- /dev/null +++ b/front/src/components/common/CopyInput.vue @@ -0,0 +1,47 @@ + + + diff --git a/front/src/components/favorites/List.vue b/front/src/components/favorites/List.vue index eacea63ec..f178e41b3 100644 --- a/front/src/components/favorites/List.vue +++ b/front/src/components/favorites/List.vue @@ -24,7 +24,7 @@
@@ -69,10 +69,11 @@ import RadioButton from '@/components/radios/Button' import Pagination from '@/components/Pagination' import OrderingMixin from '@/components/mixins/Ordering' import PaginationMixin from '@/components/mixins/Pagination' +import TranslationsMixin from '@/components/mixins/Translations' const FAVORITES_URL = 'tracks/' export default { - mixins: [OrderingMixin, PaginationMixin], + mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], components: { TrackTable, RadioButton, @@ -90,10 +91,10 @@ export default { orderingDirection: defaultOrdering.direction || '+', ordering: defaultOrdering.field, orderingOptions: [ - ['creation_date', 'Creation date'], - ['title', 'Track name'], - ['album__title', 'Album name'], - ['artist__name', 'Artist name'] + ['creation_date', 'creation_date'], + ['title', 'track_title'], + ['album__title', 'album_title'], + ['artist__name', 'artist_name'] ] } }, diff --git a/front/src/components/federation/LibraryCard.vue b/front/src/components/federation/LibraryCard.vue deleted file mode 100644 index 277606871..000000000 --- a/front/src/components/federation/LibraryCard.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - diff --git a/front/src/components/federation/LibraryFollowTable.vue b/front/src/components/federation/LibraryFollowTable.vue deleted file mode 100644 index a5dd08ced..000000000 --- a/front/src/components/federation/LibraryFollowTable.vue +++ /dev/null @@ -1,196 +0,0 @@ - - - diff --git a/front/src/components/federation/LibraryForm.vue b/front/src/components/federation/LibraryForm.vue deleted file mode 100644 index 7039cb524..000000000 --- a/front/src/components/federation/LibraryForm.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - diff --git a/front/src/components/federation/LibraryTrackTable.vue b/front/src/components/federation/LibraryTrackTable.vue deleted file mode 100644 index b24e11099..000000000 --- a/front/src/components/federation/LibraryTrackTable.vue +++ /dev/null @@ -1,228 +0,0 @@ - - - diff --git a/front/src/components/federation/LibraryWidget.vue b/front/src/components/federation/LibraryWidget.vue new file mode 100644 index 000000000..ff73bb7a8 --- /dev/null +++ b/front/src/components/federation/LibraryWidget.vue @@ -0,0 +1,84 @@ + + + diff --git a/front/src/components/globals.js b/front/src/components/globals.js index 6865ac1bc..f3bb383f0 100644 --- a/front/src/components/globals.js +++ b/front/src/components/globals.js @@ -12,6 +12,14 @@ import UserLink from '@/components/common/UserLink' Vue.component('user-link', UserLink) +import ActorLink from '@/components/common/ActorLink' + +Vue.component('actor-link', ActorLink) + +import ActorAvatar from '@/components/common/ActorAvatar' + +Vue.component('actor-avatar', ActorAvatar) + import Duration from '@/components/common/Duration' Vue.component('duration', Duration) @@ -24,4 +32,8 @@ import Message from '@/components/common/Message' Vue.component('message', Message) +import CopyInput from '@/components/common/CopyInput' + +Vue.component('copy-input', CopyInput) + export default {} diff --git a/front/src/components/library/Album.vue b/front/src/components/library/Album.vue index 312640baa..03d83e064 100644 --- a/front/src/components/library/Album.vue +++ b/front/src/components/library/Album.vue @@ -18,7 +18,7 @@ Album containing %{ count } track, by %{ artist }
-
+
Artist page @@ -45,6 +45,14 @@
+
+

+ User libraries +

+ + This album is present in the following libraries: + +
@@ -55,6 +63,7 @@ import logger from '@/logging' import backend from '@/audio/backend' import PlayButton from '@/components/audio/PlayButton' import TrackTable from '@/components/audio/track/Table' +import LibraryWidget from '@/components/federation/LibraryWidget' const FETCH_URL = 'albums/' @@ -62,7 +71,8 @@ export default { props: ['id'], components: { PlayButton, - TrackTable + TrackTable, + LibraryWidget }, data () { return { diff --git a/front/src/components/library/Artist.vue b/front/src/components/library/Artist.vue index a507ae403..fe74f6f17 100644 --- a/front/src/components/library/Artist.vue +++ b/front/src/components/library/Artist.vue @@ -23,7 +23,7 @@ - + Play all albums @@ -56,6 +56,14 @@ +
+

+ User libraries +

+ + This artist is present in the following libraries: + +
@@ -69,6 +77,7 @@ import AlbumCard from '@/components/audio/album/Card' import RadioButton from '@/components/radios/Button' import PlayButton from '@/components/audio/PlayButton' import TrackTable from '@/components/audio/track/Table' +import LibraryWidget from '@/components/federation/LibraryWidget' export default { props: ['id'], @@ -76,7 +85,8 @@ export default { AlbumCard, RadioButton, PlayButton, - TrackTable + TrackTable, + LibraryWidget }, data () { return { @@ -135,6 +145,11 @@ export default { return a + b }) + this.tracks.length }, + isPlayable () { + return this.artist.albums.filter((a) => { + return a.is_playable + }).length > 0 + }, wikipediaUrl () { return 'https://en.wikipedia.org/w/index.php?search=' + this.artist.name }, diff --git a/front/src/components/library/Artists.vue b/front/src/components/library/Artists.vue index 1c4849cc3..379d07e4b 100644 --- a/front/src/components/library/Artists.vue +++ b/front/src/components/library/Artists.vue @@ -16,7 +16,7 @@ @@ -77,13 +77,14 @@ import logger from '@/logging' import OrderingMixin from '@/components/mixins/Ordering' import PaginationMixin from '@/components/mixins/Pagination' +import TranslationsMixin from '@/components/mixins/Translations' import ArtistCard from '@/components/audio/artist/Card' import Pagination from '@/components/Pagination' const FETCH_URL = 'artists/' export default { - mixins: [OrderingMixin, PaginationMixin], + mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], props: { defaultQuery: {type: String, required: false, default: ''} }, @@ -102,8 +103,8 @@ export default { orderingDirection: defaultOrdering.direction || '+', ordering: defaultOrdering.field, orderingOptions: [ - ['creation_date', 'Creation date'], - ['name', 'Name'] + ['creation_date', 'creation_date'], + ['name', 'name'] ] } }, @@ -143,7 +144,7 @@ export default { page_size: this.paginateBy, name__icontains: this.query, ordering: this.getOrderingAsString(), - listenable: 'true' + playable: 'true' } logger.default.debug('Fetching artists') axios.get(url, {params: params}).then((response) => { diff --git a/front/src/components/library/FileUpload.vue b/front/src/components/library/FileUpload.vue new file mode 100644 index 000000000..e637210f2 --- /dev/null +++ b/front/src/components/library/FileUpload.vue @@ -0,0 +1,316 @@ + + + + + + diff --git a/front/src/components/library/import/FileUploadWidget.vue b/front/src/components/library/FileUploadWidget.vue similarity index 85% rename from front/src/components/library/import/FileUploadWidget.vue rename to front/src/components/library/FileUploadWidget.vue index 1de8090c9..93bead3e7 100644 --- a/front/src/components/library/import/FileUploadWidget.vue +++ b/front/src/components/library/FileUploadWidget.vue @@ -19,7 +19,9 @@ export default { form.append(key, value) } } - form.append(this.name, file.file, file.file.filename || file.name) + let filename = file.file.filename || file.name + form.append('source', `upload://${filename}`) + form.append(this.name, file.file, filename) let xhr = new XMLHttpRequest() xhr.open('POST', file.postAction) xhr.setRequestHeader('Authorization', this.$store.getters['auth/header']) diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue index 0bb16e1dd..e11127608 100644 --- a/front/src/components/library/Home.vue +++ b/front/src/components/library/Home.vue @@ -13,7 +13,7 @@
- +
@@ -21,7 +21,7 @@
- +
@@ -72,7 +72,7 @@ export default { this.isLoadingArtists = true let params = { ordering: '-creation_date', - listenable: true + playable: true } let url = ARTISTS_URL logger.default.time('Loading latest artists') diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue index db2ac27b4..01a357724 100644 --- a/front/src/components/library/Library.vue +++ b/front/src/components/library/Library.vue @@ -13,14 +13,6 @@ Playlists -
diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue index 2b542a71b..4cc1d58df 100644 --- a/front/src/components/library/Radios.vue +++ b/front/src/components/library/Radios.vue @@ -34,7 +34,7 @@ @@ -99,13 +99,14 @@ import logger from '@/logging' import OrderingMixin from '@/components/mixins/Ordering' import PaginationMixin from '@/components/mixins/Pagination' +import TranslationsMixin from '@/components/mixins/Translations' import RadioCard from '@/components/radios/Card' import Pagination from '@/components/Pagination' const FETCH_URL = 'radios/radios/' export default { - mixins: [OrderingMixin, PaginationMixin], + mixins: [OrderingMixin, PaginationMixin, TranslationsMixin], props: { defaultQuery: {type: String, required: false, default: ''} }, @@ -124,8 +125,8 @@ export default { orderingDirection: defaultOrdering.direction || '+', ordering: defaultOrdering.field, orderingOptions: [ - ['creation_date', 'Creation date'], - ['name', 'Name'] + ['creation_date', 'creation_date'], + ['name', 'name'] ] } }, diff --git a/front/src/components/library/Track.vue b/front/src/components/library/Track.vue index 2a58536d8..75d5f650d 100644 --- a/front/src/components/library/Track.vue +++ b/front/src/components/library/Track.vue @@ -44,13 +44,13 @@ View on MusicBrainz - + Download -
+

Track information

@@ -58,8 +58,8 @@ - - - -
Duration - {{ time.parse(file.duration) }} + + {{ time.parse(track.duration) }} N/A @@ -69,8 +69,8 @@ Size - {{ file.size | humanSize }} + + {{ track.size | humanSize }} N/A @@ -80,8 +80,8 @@ Bitrate - {{ file.bitrate | humanSize }}/s + + {{ track.bitrate | humanSize }}/s N/A @@ -91,8 +91,8 @@ Type - {{ file.mimetype }} + + {{ track.mimetype }} N/A @@ -118,6 +118,14 @@ +
+

+ User libraries +

+ + This track is present in the following libraries: + +
@@ -131,6 +139,7 @@ import logger from '@/logging' import PlayButton from '@/components/audio/PlayButton' import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon' import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon' +import LibraryWidget from '@/components/federation/LibraryWidget' const FETCH_URL = 'tracks/' @@ -139,7 +148,8 @@ export default { components: { PlayButton, TrackPlaylistIcon, - TrackFavoriteIcon + TrackFavoriteIcon, + LibraryWidget }, data () { return { @@ -192,16 +202,11 @@ export default { return 'https://musicbrainz.org/recording/' + this.track.mbid }, downloadUrl () { - if (this.track.files.length > 0) { - let u = this.$store.getters['instance/absoluteUrl'](this.track.files[0].path) - if (this.$store.state.auth.authenticated) { - u = url.updateQueryString(u, 'jwt', this.$store.state.auth.token) - } - return u + let u = this.$store.getters['instance/absoluteUrl'](this.track.listen_url) + if (this.$store.state.auth.authenticated) { + u = url.updateQueryString(u, 'jwt', encodeURI(this.$store.state.auth.token)) } - }, - file () { - return this.track.files[0] + return u }, lyricsSearchUrl () { let base = 'http://lyrics.wikia.com/wiki/Special:Search?query=' diff --git a/front/src/components/library/import/ArtistImport.vue b/front/src/components/library/import/ArtistImport.vue deleted file mode 100644 index f86f71cce..000000000 --- a/front/src/components/library/import/ArtistImport.vue +++ /dev/null @@ -1,170 +0,0 @@ - - - diff --git a/front/src/components/library/import/BatchDetail.vue b/front/src/components/library/import/BatchDetail.vue deleted file mode 100644 index fc5801ed1..000000000 --- a/front/src/components/library/import/BatchDetail.vue +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - diff --git a/front/src/components/library/import/BatchList.vue b/front/src/components/library/import/BatchList.vue deleted file mode 100644 index 9ef6bd9cd..000000000 --- a/front/src/components/library/import/BatchList.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - diff --git a/front/src/components/library/import/FileUpload.vue b/front/src/components/library/import/FileUpload.vue deleted file mode 100644 index a9b685523..000000000 --- a/front/src/components/library/import/FileUpload.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - diff --git a/front/src/components/library/import/ImportMixin.vue b/front/src/components/library/import/ImportMixin.vue deleted file mode 100644 index 8b0757dcc..000000000 --- a/front/src/components/library/import/ImportMixin.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - diff --git a/front/src/components/library/import/Main.vue b/front/src/components/library/import/Main.vue deleted file mode 100644 index 08047fd2f..000000000 --- a/front/src/components/library/import/Main.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - - - diff --git a/front/src/components/library/import/ReleaseImport.vue b/front/src/components/library/import/ReleaseImport.vue deleted file mode 100644 index 0ec789030..000000000 --- a/front/src/components/library/import/ReleaseImport.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - diff --git a/front/src/components/library/import/TrackImport.vue b/front/src/components/library/import/TrackImport.vue deleted file mode 100644 index 10a146344..000000000 --- a/front/src/components/library/import/TrackImport.vue +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - - diff --git a/front/src/components/manage/library/FilesTable.vue b/front/src/components/manage/library/FilesTable.vue index 28d2f3997..1f5fc3976 100644 --- a/front/src/components/manage/library/FilesTable.vue +++ b/front/src/components/manage/library/FilesTable.vue @@ -10,7 +10,7 @@ @@ -32,7 +32,7 @@ @action-launched="fetchData" :objects-data="result" :actions="actions" - :action-url="'manage/library/track-files/action/'" + :action-url="'manage/library/uploads/action/'" :filters="actionFilters">