diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 81962433815e01f1897804005565959d3b48235c..d8fca1274aeb03abafe37cc8c5718a079f7782fa 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -358,6 +358,344 @@ Internationalization -------------------- We're using https://github.com/Polyconseil/vue-gettext to manage i18n in the project. +<<<<<<< HEAD +When working on the front-end, any end-user string should be marked as a translatable string, +with the proper context, as described below. + +Translations in HTML +^^^^^^^^^^^^^^^^^^^^ + +Translations in HTML use the ``<translate>`` tag:: + + <template> + <div> + <h1><translate translate-context="Content/Profile/Header">User profile</translate></h1> + <p> + <translate + translate-context="Content/Profile/Paragraph" + :translate-params="{username: 'alice'}"> + You are logged in as %{ username } + </translate> + </p> + <p> + <translate + translate-context="Content/Profile/Paragraph" + translate-plural="You have %{ count } new messages, that's a lot!" + :translate-n="unreadMessagesCount" + :translate-params="{count: unreadMessagesCount}"> + You have 1 new message + </translate> + </p> + </div> + </template> + +Anything between the `<translate>` and `</translate>` delimiters will be considered as a translatable string. +You can use variables in the translated string via the ``:translate-params="{var: 'value'}"`` directive, and reference them like this: +``val value is %{ value }``. + +For pluralization, you need to use ``translate-params`` in conjunction with ``translate-plural`` and ``translate-n``: + +- ``translate-params`` should contain the variable you're using for pluralization (which is usually shown to the user) +- ``translate-n`` should match the same variable +- The ``<translate>`` delimiters contain the non-pluralized version of your string +- The ``translate-plural`` directive contains the pluralized version of your string + + +Translations in javascript +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Translations in javascript work by calling the ``this.$*gettext`` functions:: + + export default { + computed: { + strings () { + let tracksCount = 42 + let playButton = this.$pgettext('Sidebar/Player/Button/Verb, Short', 'Play') + let loginMessage = this.$pgettext('*/Login/Message', 'Welcome back %{ username }') + let addedMessage = this.$npgettext('*/Player/Message', 'One track was queued', '%{ count } tracks were queued', tracksCount) + console.log(this.$gettextInterpolate(addedMessage, {count: tracksCount})) + console.log(this.$gettextInterpolate(loginMessage, {username: 'alice'})) + } + } + } + +The first argument of the ``$pgettext`` and ``$npgettext`` functions is the string context. + +Contextualization +^^^^^^^^^^^^^^^^^ + +Translation contexts provided via the ``translate-context`` directive and the ``$pgettext`` and ``$npgettext`` are never shown to end users +but visible by Funkwhale translators. They help translators where and how the strings are used, +especially with short or ambiguous strings, like ``May``, which can refer a month or a verb. + +While we could in theory use free form context, like ``This string is inside a button, in the main page, and is a call to action``, +Funkwhale use a hierarchical structure to write contexts and keep them short and consistents accross the app. The previous context, +rewritten correctly would be: ``Content/Home/Button/Call to action``. + +This hierarchical structure is made of several parts: + +- The location part, which is required and refers to the big blocks found in Funkwhale UI where the translated string is displayed: + - ``Content`` + - ``Footer`` + - ``Head`` + - ``Menu`` + - ``Popup`` + - ``Sidebar`` + - ``*`` for strings that are not tied to a specific location + +- The feature part, which is required, and refers to the feature associated with the translated string: + - ``About`` + - ``Admin`` + - ``Album`` + - ``Artist`` + - ``Embed`` + - ``Home`` + - ``Login`` + - ``Library`` + - ``Moderation`` + - ``Player`` + - ``Playlist`` + - ``Profile`` + - ``Favorites`` + - ``Notifications`` + - ``Radio`` + - ``Search`` + - ``Settings`` + - ``Signup`` + - ``Track`` + - ``Queue`` + - ``*`` for strings that are not tied to a specific feature + +- The component part, which is required and refers to the type of element that contain the string: + - ``Button`` + - ``Card`` + - ``Checkbox`` + - ``Dropdown`` + - ``Error message`` + - ``Form`` + - ``Header`` + - ``Help text`` + - ``Hidden text`` + - ``Icon`` + - ``Input`` + - ``Image`` + - ``Label`` + - ``Link`` + - ``List item`` + - ``Menu`` + - ``Message`` + - ``Paragraph`` + - ``Placeholder`` + - ``Tab`` + - ``Table`` + - ``Title`` + - ``Tooltip`` + - ``*`` for strings that are not tied to a specific component + +The detail part, which is optional and refers to the contents of the string itself, such as: + - ``Adjective`` + - ``Call to action`` + - ``Noun`` + - ``Short`` + - ``Unit`` + - ``Verb`` + +Here are a few examples of valid context hierarchies: + +- ``Sidebar/Player/Button`` +- ``Content/Home/Button/Call to action`` +- ``Footer/*/Help text`` +- ``*/*/*/Verb, Short`` +- ``Popup/Playlist/Button`` +- ``Content/Admin/Table.Label/Short, Noun (Value is a date)`` + +It's possible to nest multiple component parts to reach a higher level of detail. The component parts are then separated by a dot: + +- ``Sidebar/Queue/Tab.Title`` +- ``Content/*/Button.Title`` +- ``Content/*/Table.Header`` +- ``Footer/*/List item.Link`` +- ``Content/*/Form.Help text`` + +Collecting translatable strings +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you want to ensure your translatable strings are correctly marked for translation, +you can try to extract them. +||||||| merged common ancestors +When working on the front-end, any end-user string should be translated +using either ``<translate>yourstring</translate>`` or ``$gettext('yourstring')`` +function. +======= +<<<<<<< HEAD +When working on the front-end, any end-user string should be translated +using either ``<translate>yourstring</translate>`` or ``$gettext('yourstring')`` +function. +||||||| parent of 21fb39dd... Update docs/developers/index.rst, docs/developers/subsonic.rst files +When working on the front-end, any end-user string should be marked as a translatable string, +with the proper context, as described below. + +Translations in HTML +^^^^^^^^^^^^^^^^^^^^ + +Translations in HTML use the ``<translate>`` tag:: + + <template> + <div> + <h1><translate translate-context="Content/Profile/Header">User profile</translate></h1> + <p> + <translate + translate-context="Content/Profile/Paragraph" + :translate-params="{username: 'alice'}"> + You are logged in as %{ username } + </translate> + </p> + <p> + <translate + translate-context="Content/Profile/Paragraph" + translate-plural="You have %{ count } new messages, that's a lot!" + :translate-n="unreadMessagesCount" + :translate-params="{count: unreadMessagesCount}"> + You have 1 new message + </translate> + </p> + </div> + </template> + +Anything between the `<translate>` and `</translate>` delimiters will be considered as a translatable string. +You can use variables in the translated string via the ``:translate-params="{var: 'value'}"`` directive, and reference them like this: +``val value is %{ value }``. + +For pluralization, you need to use ``translate-params`` in conjunction with ``translate-plural`` and ``translate-n``: + +- ``translate-params`` should contain the variable you're using for pluralization (which is usually shown to the user) +- ``translate-n`` should match the same variable +- The ``<translate>`` delimiters contain the non-pluralized version of your string +- The ``translate-plural`` directive contains the pluralized version of your string + + +Translations in javascript +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Translations in javascript work by calling the ``this.$*gettext`` functions:: + + export default { + computed: { + strings () { + let tracksCount = 42 + let playButton = this.$pgettext('Sidebar/Player/Button/Verb, Short', 'Play') + let loginMessage = this.$pgettext('*/Login/Message', 'Welcome back %{ username }') + let addedMessage = this.$npgettext('*/Player/Message', 'One track was queued', '%{ count } tracks were queued', tracksCount) + console.log(this.$gettextInterpolate(addedMessage, {count: tracksCount})) + console.log(this.$gettextInterpolate(loginMessage, {username: 'alice'})) + } + } + } + +The first argument of the ``$pgettext`` and ``$npgettext`` functions is the string context. + +Contextualization +^^^^^^^^^^^^^^^^^ + +Translation contexts provided via the ``translate-context`` directive and the ``$pgettext`` and ``$npgettext`` are never shown to end users +but visible by Funkwhale translators. They help translators where and how the strings are used, +especially with short or ambiguous strings, like ``May``, which can refer a month or a verb. + +While we could in theory use free form context, like ``This string is inside a button, in the main page, and is a call to action``, +Funkwhale use a hierarchical structure to write contexts and keep them short and consistents accross the app. The previous context, +rewritten correctly would be: ``Content/Home/Button/Call to action``. + +This hierarchical structure is made of several parts: + +- The location part, which is required and refers to the big blocks found in Funkwhale UI where the translated string is displayed: + - ``Content`` + - ``Footer`` + - ``Head`` + - ``Menu`` + - ``Popup`` + - ``Sidebar`` + - ``*`` for strings that are not tied to a specific location + +- The feature part, which is required, and refers to the feature associated with the translated string: + - ``About`` + - ``Admin`` + - ``Album`` + - ``Artist`` + - ``Embed`` + - ``Home`` + - ``Login`` + - ``Library`` + - ``Moderation`` + - ``Player`` + - ``Playlist`` + - ``Profile`` + - ``Favorites`` + - ``Notifications`` + - ``Radio`` + - ``Search`` + - ``Settings`` + - ``Signup`` + - ``Track`` + - ``Queue`` + - ``*`` for strings that are not tied to a specific feature + +- The component part, which is required and refers to the type of element that contain the string: + - ``Button`` + - ``Card`` + - ``Checkbox`` + - ``Dropdown`` + - ``Error message`` + - ``Form`` + - ``Header`` + - ``Help text`` + - ``Hidden text`` + - ``Icon`` + - ``Input`` + - ``Image`` + - ``Label`` + - ``Link`` + - ``List item`` + - ``Menu`` + - ``Message`` + - ``Paragraph`` + - ``Placeholder`` + - ``Tab`` + - ``Table`` + - ``Title`` + - ``Tooltip`` + - ``*`` for strings that are not tied to a specific component + +The detail part, which is optional and refers to the contents of the string itself, such as: + - ``Adjective`` + - ``Call to action`` + - ``Noun`` + - ``Short`` + - ``Unit`` + - ``Verb`` + +Here are a few examples of valid context hierarchies: + +- ``Sidebar/Player/Button`` +- ``Content/Home/Button/Call to action`` +- ``Footer/*/Help text`` +- ``*/*/*/Verb, Short`` +- ``Popup/Playlist/Button`` +- ``Content/Admin/Table.Label/Short, Noun (Value is a date)`` + +It's possible to nest multiple component parts to reach a higher level of detail. The component parts are then separated by a dot: + +- ``Sidebar/Queue/Tab.Title`` +- ``Content/*/Button.Title`` +- ``Content/*/Table.Header`` +- ``Footer/*/List item.Link`` +- ``Content/*/Form.Help text`` + +Collecting translatable strings +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you want to ensure your translatable strings are correctly marked for translation, +you can try to extract them. + When working on the front-end, any end-user string should be marked as a translatable string, with the proper context, as described below. diff --git a/api/funkwhale_api/federation/admin.py b/api/funkwhale_api/federation/admin.py index acb2e5b67b33830e57550d9484516d0071c8c9df..8c9bbe31c8062bda8fd9192b14170f5289620b13 100644 --- a/api/funkwhale_api/federation/admin.py +++ b/api/funkwhale_api/federation/admin.py @@ -33,8 +33,8 @@ class DomainAdmin(admin.ModelAdmin): @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"] + search_fields = ["payload", "fid", "url", "actor__domain__name"] + list_filter = ["type", "actor__domain__name"] actions = [redeliver_activities] list_select_related = True @@ -49,7 +49,7 @@ class ActorAdmin(admin.ModelAdmin): "creation_date", "last_fetch_date", ] - search_fields = ["fid", "domain", "preferred_username"] + search_fields = ["fid", "domain__name", "preferred_username"] list_filter = ["type"] diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 44043b16cf28b3a7adce65f83db091b6a6abc135..0c5ce549796f9a7f03d11a3d475096bdff598ffa 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -859,7 +859,7 @@ class TrackSerializer(MusicEntitySerializer): 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) + track = music_tasks.get_track_from_import_metadata(metadata, update_cover=True) return track diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index c8fed58410d9abacd4f5872451a48238b8988840..946d7a411f9de1e1eb6089fd148974718e20ea65 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -26,7 +26,9 @@ from . import serializers logger = logging.getLogger(__name__) -def update_album_cover(album, source=None, cover_data=None, replace=False): +def update_album_cover( + album, source=None, cover_data=None, musicbrainz=True, replace=False +): if album.cover and not replace: return if cover_data: @@ -39,7 +41,7 @@ def update_album_cover(album, source=None, cover_data=None, replace=False): cover = get_cover_from_fs(path) if cover: return album.get_image(data=cover) - if album.mbid: + if musicbrainz and album.mbid: try: logger.info( "[Album %s] Fetching cover from musicbrainz release %s", @@ -179,8 +181,8 @@ def process_upload(upload): import_metadata = upload.import_metadata or {} old_status = upload.import_status audio_file = upload.get_audio_file() + additional_data = {} try: - additional_data = {} if not audio_file: # we can only rely on user proveded data final_metadata = import_metadata @@ -241,6 +243,15 @@ def process_upload(upload): "bitrate", ] ) + + # update album cover, if needed + if not track.album.cover: + update_album_cover( + track.album, + source=final_metadata.get("upload_source"), + cover_data=final_metadata.get("cover_data"), + ) + broadcast = getter( import_metadata, "funkwhale", "config", "broadcast", default=True ) @@ -369,7 +380,18 @@ def sort_candidates(candidates, important_fields): @transaction.atomic -def get_track_from_import_metadata(data): +def get_track_from_import_metadata(data, update_cover=False): + track = _get_track(data) + if update_cover and track and not track.album.cover: + update_album_cover( + track.album, + source=data.get("upload_source"), + cover_data=data.get("cover_data"), + ) + return track + + +def _get_track(data): track_uuid = getter(data, "funkwhale", "track", "uuid") if track_uuid: @@ -380,12 +402,6 @@ def get_track_from_import_metadata(data): 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) @@ -479,10 +495,6 @@ def get_track_from_import_metadata(data): 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"] diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py index 023e40cac29a89168e497977543f5995dece73b5..a53ad464038316f5efbd5810bd8a995e33a744ec 100644 --- a/api/funkwhale_api/subsonic/serializers.py +++ b/api/funkwhale_api/subsonic/serializers.py @@ -70,6 +70,7 @@ def get_track_data(album, track, upload): "album": album.title, "artist": album.artist.name, "track": track.position or 1, + "discNumber": track.disc_number or 1, "contentType": upload.mimetype, "suffix": upload.extension or "", "duration": upload.duration or 0, diff --git a/api/tests/music/test_tasks.py b/api/tests/music/test_tasks.py index af1a9557db70ffcb266d2b354ac4acebd38ddd4e..ac62c31b86c3ebc7d893ac9b79361ab59792d516 100644 --- a/api/tests/music/test_tasks.py +++ b/api/tests/music/test_tasks.py @@ -153,7 +153,7 @@ def test_can_create_track_from_file_metadata_federation(factories, mocker, r_moc 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) + track = tasks.get_track_from_import_metadata(metadata, update_cover=True) assert track.title == metadata["title"] assert track.fid == metadata["fid"] @@ -183,7 +183,9 @@ def test_sort_candidates(factories): def test_upload_import(now, factories, temp_signal, mocker): outbox = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch") - track = factories["music.Track"]() + update_album_cover = mocker.patch("funkwhale_api.music.tasks.update_album_cover") + get_picture = mocker.patch("funkwhale_api.music.metadata.Metadata.get_picture") + track = factories["music.Track"](album__cover="") upload = factories["music.Upload"]( track=None, import_metadata={"funkwhale": {"track": {"uuid": str(track.uuid)}}} ) @@ -196,6 +198,10 @@ def test_upload_import(now, factories, temp_signal, mocker): assert upload.track == track assert upload.import_status == "finished" assert upload.import_date == now + get_picture.assert_called_once_with("cover_front", "other") + update_album_cover.assert_called_once_with( + upload.track.album, cover_data=get_picture.return_value, source=upload.source + ) handler.assert_called_once_with( upload=upload, old_status="pending", diff --git a/api/tests/subsonic/test_serializers.py b/api/tests/subsonic/test_serializers.py index 85cb65fa791349df787dc3e742afc88ade5442d9..2c468a4f027e20b01d975552e8a841464e2e6d4a 100644 --- a/api/tests/subsonic/test_serializers.py +++ b/api/tests/subsonic/test_serializers.py @@ -64,7 +64,7 @@ def test_get_artist_serializer(factories): def test_get_album_serializer(factories): artist = factories["music.Artist"]() album = factories["music.Album"](artist=artist) - track = factories["music.Track"](album=album) + track = factories["music.Track"](album=album, disc_number=42) upload = factories["music.Upload"](track=track, bitrate=42000, duration=43, size=44) expected = { @@ -85,6 +85,7 @@ def test_get_album_serializer(factories): "album": album.title, "artist": artist.name, "track": track.position, + "discNumber": track.disc_number, "year": track.album.release_date.year, "contentType": upload.mimetype, "suffix": upload.extension or "", diff --git a/changes/changelog.d/757.bugfix b/changes/changelog.d/757.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..f9e9eae351a63297a17b790e957ad7dabba292bf --- /dev/null +++ b/changes/changelog.d/757.bugfix @@ -0,0 +1 @@ +Ensure cover art from uploaded files is picked up properly on existing albums (#757) diff --git a/changes/changelog.d/764.bugfix b/changes/changelog.d/764.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..114f602b5744cf750341a5d9c995173de638854b --- /dev/null +++ b/changes/changelog.d/764.bugfix @@ -0,0 +1 @@ +Fixed broken sample apache configuration (#764) diff --git a/changes/changelog.d/765.bugfix b/changes/changelog.d/765.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..79872e1ed5bd06c9d95398e37032b9abd2ba00da --- /dev/null +++ b/changes/changelog.d/765.bugfix @@ -0,0 +1 @@ +Include disc number in Subsonic responses (#765) diff --git a/changes/changelog.d/766.enhancement b/changes/changelog.d/766.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..7aca9fb1c041d8119f52db134b24ecc306920945 --- /dev/null +++ b/changes/changelog.d/766.enhancement @@ -0,0 +1 @@ +Added title on hover for truncated content (#766) diff --git a/changes/changelog.d/767.bugfix b/changes/changelog.d/767.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..8a2bbdb86c9442373863c36323517b5db4e2f0d0 --- /dev/null +++ b/changes/changelog.d/767.bugfix @@ -0,0 +1 @@ +Fixed broken Activity and Actor modules in django admin (#767) diff --git a/deploy/apache.conf b/deploy/apache.conf index 52eee9533308ff3f4a51a2df019d7026e582d296..e1409a627d3d4e76ec4a1901ace204ddf6efb374 100644 --- a/deploy/apache.conf +++ b/deploy/apache.conf @@ -65,7 +65,9 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music </Proxy> # Activating WebSockets - ProxyPass "/api/v1/activity" ${funkwhale-api-ws}/api/v1/activity + <Location "/api/v1/activity"> + ProxyPass ${funkwhale-api-ws}/api/v1/activity + </Location> <Location "/"> # similar to nginx 'client_max_body_size 100M;' @@ -90,13 +92,19 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music ProxyPassReverse ${funkwhale-api}/.well-known/ </Location> - ProxyPass "/front" "!" + <Location "/front"> + ProxyPass "!" + </Location> Alias /front /srv/funkwhale/front/dist - ProxyPass "/media" "!" + <Location "/media"> + ProxyPass "!" + </Location> Alias /media /srv/funkwhale/data/media - ProxyPass "/staticfiles" "!" + <Location "/staticfiles"> + ProxyPass "!" + </Location> Alias /staticfiles /srv/funkwhale/data/static # Setting appropriate access levels to serve frontend diff --git a/dev.yml b/dev.yml index d5170f61ebe4ed0e5825fed1d3c0a8208755bee2..d0ffc62e433b3106cc08e91245c0f0d3b75b0f07 100644 --- a/dev.yml +++ b/dev.yml @@ -52,7 +52,7 @@ services: command: python /app/manage.py runserver 0.0.0.0:${FUNKWHALE_API_PORT-5000} volumes: - ./api:/app - - "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro" + - "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro" environment: - "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}" - "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test" @@ -87,7 +87,7 @@ services: - "CACHE_URL=redis://redis:6379/0" volumes: - ./api:/app - - "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro" + - "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro" networks: - internal nginx: @@ -112,7 +112,7 @@ services: volumes: - ./docker/nginx/conf.dev:/etc/nginx/nginx.conf.template:ro - ./docker/nginx/entrypoint.sh:/entrypoint.sh:ro - - "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro" + - "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro" - ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro - "${MEDIA_ROOT-./api/funkwhale_api/media}:/protected/media:ro" networks: diff --git a/docs/backup.rst b/docs/backup.rst new file mode 100644 index 0000000000000000000000000000000000000000..d474678ae3ba48a5c0a51b9114200673148c894e --- /dev/null +++ b/docs/backup.rst @@ -0,0 +1,79 @@ +Backup your Funkwhale instance +============================== + +.. note:: + + Before upgrading your instance, we strongly advise you to make at least a database backup. Ideally, you should make a full backup, including the database and the media files. + + +Docker setup +------------ + +If you've followed the setup instructions in :doc:`../installation/docker`, here is the backup path: + +Multi-container installation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Backup the db +^^^^^^^^^^^^^ + +On docker setups, you have to ``pg_dumpall`` in container ``funkwhale_postgres_1``: + +.. code-block:: shell + + docker exec -t funkwhale_postgres_1 pg_dumpall -c -U postgres > dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql + +Backup the media files +^^^^^^^^^^^^^^^^^^^^^^ + +To backup docker data volumes, as the volumes are bound mounted to the host, the ``rsync`` way would go like this: + +.. code-block:: shell + + rsync -avzhP /srv/funkwhale/data/media /path/to/your/backup/media + rsync -avzhP /srv/funkwhale/data/music /path/to/your/backup/music + + +Backup the configuration files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On docker setups, the configuration file is located at the root level: + +.. code-block:: shell + + rsync -avzhP /srv/funkwhale/.env /path/to/your/backup/.env + + +Non-docker setup +---------------- + +Backup the db +^^^^^^^^^^^^^ + +On non-docker setups, you have to ``pg_dump`` as user ``postgres``: + +.. code-block:: shell + + sudo -u postgres -H pg_dump funkwhale > /path/to/your/backup/dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql + +Backup the media files +^^^^^^^^^^^^^^^^^^^^^^ + +A simple way to backup your media files is to use ``rsync``: + +.. code-block:: shell + + rsync -avzhP /srv/funkwhale/data/media /path/to/your/backup/media + rsync -avzhP /srv/funkwhale/data/music /path/to/your/backup/music + +Backup the configuration files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: shell + + rsync -avzhP /srv/funkwhale/config/.env /path/to/your/backup/.env + +.. note:: + You may also want to backup your proxy configuration file. + + For frequent backups, you may want to use deduplication and compression to keep the backup size low. In this case, a tool like ``borg`` will be more appropriate. diff --git a/docs/users/index.rst b/docs/users/index.rst index 86dbf62d5f1e7a886227936212d99ac0536bdac2..aa05555bd93f60256087c1d9f544f1c0660c1f3f 100644 --- a/docs/users/index.rst +++ b/docs/users/index.rst @@ -34,5 +34,5 @@ Troubleshooting Issues .. toctree:: :maxdepth: 2 - + troubleshooting diff --git a/docs/users/upload.rst b/docs/users/upload.rst index c6f5646f0a3f2c0ac5481ec35ba56bef613fb872..1dfa4d76fb73f2ed3bf2a8ee70a3db69f908f4f6 100644 --- a/docs/users/upload.rst +++ b/docs/users/upload.rst @@ -1,5 +1,5 @@ -Uploading and removing content on Funkwhale -=========================================== +Uploading Content To Funkwhale +============================== To upload content to any Funkwhale instance, you need: @@ -149,7 +149,7 @@ can vary depending on server load. Removing files -------------- -If you want to remove some of the files you have uploaded, visit ``/content/libraries/tracks/`` or click "Add content" in the sidebar then "Tracks" in the top menu. +If you want to remove some of the files you have uploaded, visit ``/content/libraries/tracks/`` or click "Add content" in the sidebar then "Tracks" in the top menu. Then select the files you want to delete using the checkboxes on the left ; you can filter the list of files using a search pattern. Finally, select "Delete" in the "Action" menu and click "Go". diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index caa402cf32dee93916cf8256b440417413a78a48..213ab92ce9092cbe073422ce02386396fdd942ad 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -146,9 +146,11 @@ <img class="ui mini image" v-else src="../assets/audio/default-cover.png"> </td> <td colspan="4"> - <button class="title reset ellipsis" :aria-label="labels.selectTrack"> + <button class="title reset ellipsis" :title="track.title" :aria-label="labels.selectTrack"> <strong>{{ track.title }}</strong><br /> - {{ track.artist.name }} + <span> + {{ track.artist.name }} + </span> </button> </td> <td> diff --git a/front/src/components/audio/album/Card.vue b/front/src/components/audio/album/Card.vue index d498e4e355d4caf517b1688f619ad2e7e8694e26..54ae9f254c180df940ed32dcf30837ee7a7b9225 100644 --- a/front/src/components/audio/album/Card.vue +++ b/front/src/components/audio/album/Card.vue @@ -10,7 +10,7 @@ </div> <div class="meta"> <span> - <router-link tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}"> + <router-link :title="album.artist.name" tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}"> <span v-translate="{artist: album.artist.name}" translate-context="Content/Album/Card" :translate-params="{artist: album.artist.name}">By %{ artist }</span> </router-link> </span><span class="time" v-if="album.release_date">– {{ album.release_date | year }}</span> @@ -24,7 +24,7 @@ </td> <td class="content-cell" colspan="5"> <track-favorite-icon :track="track"></track-favorite-icon> - <router-link class="track discrete link" :to="{name: 'library.tracks.detail', params: {id: track.id }}"> + <router-link :title="track.title" class="track discrete link" :to="{name: 'library.tracks.detail', params: {id: track.id }}"> <template v-if="track.position"> {{ track.position }}. </template> diff --git a/front/src/components/audio/artist/Card.vue b/front/src/components/audio/artist/Card.vue index 9e018216c07906313d84447ca28f3177d80af772..b06cdcd75b126c3d5b7f68abce5923caf0c18a2a 100644 --- a/front/src/components/audio/artist/Card.vue +++ b/front/src/components/audio/artist/Card.vue @@ -15,7 +15,7 @@ <img class="ui mini image" v-else src="../../../assets/audio/default-cover.png"> </td> <td colspan="4"> - <router-link class="discrete link" :to="{name: 'library.albums.detail', params: {id: album.id }}"> + <router-link :title="album.title" class="discrete link" :to="{name: 'library.albums.detail', params: {id: album.id }}"> <strong>{{ album.title }}</strong> </router-link><br /> {{ album.tracks_count }} tracks diff --git a/front/src/components/audio/track/Row.vue b/front/src/components/audio/track/Row.vue index 690fe48f5b89ac50d566d0256dae6d09f7e8566e..abc4137bf397306b457cb19e8e9d39e6b21c0838 100644 --- a/front/src/components/audio/track/Row.vue +++ b/front/src/components/audio/track/Row.vue @@ -8,7 +8,7 @@ <img class="ui mini image" v-else src="../../../assets/audio/default-cover.png"> </td> <td colspan="6"> - <router-link class="track" :to="{name: 'library.tracks.detail', params: {id: track.id }}"> + <router-link class="track" :title="track.title" :to="{name: 'library.tracks.detail', params: {id: track.id }}"> <template v-if="displayPosition && track.position"> {{ track.position }}. </template> @@ -16,21 +16,21 @@ </router-link> </td> <td colspan="4"> - <router-link v-if="track.artist.id === albumArtist.id" class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}"> + <router-link v-if="track.artist.id === albumArtist.id" :title="track.artist.name" class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}"> {{ track.artist.name }} </router-link> <template v-else> - <router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: albumArtist.id }}"> + <router-link class="artist discrete link" :title="albumArtist.name" :to="{name: 'library.artists.detail', params: {id: albumArtist.id }}"> {{ albumArtist.name }} </router-link> / - <router-link class="artist discrete link" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}"> + <router-link class="artist discrete link" :title="track.artist.name" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}"> {{ track.artist.name }} </router-link> </template> </td> <td colspan="4"> - <router-link class="album discrete link" :to="{name: 'library.albums.detail', params: {id: track.album.id }}"> + <router-link class="album discrete link" :title="track.album.title" :to="{name: 'library.albums.detail', params: {id: track.album.id }}"> {{ track.album.title }} </router-link> </td> diff --git a/front/src/components/playlists/PlaylistModal.vue b/front/src/components/playlists/PlaylistModal.vue index a3ffbd51eea94f6ac710288871043c9d0dde96af..522f150fcaef5ab24e0db7f41b43c8f663eaca2f 100644 --- a/front/src/components/playlists/PlaylistModal.vue +++ b/front/src/components/playlists/PlaylistModal.vue @@ -43,7 +43,7 @@ class="ui icon basic small button" :to="{name: 'library.playlists.detail', params: {id: playlist.id }, query: {mode: 'edit'}}"><i class="ui pencil icon"></i></router-link> </td> - <td> + <td :title="playlist.name"> <router-link :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">{{ playlist.name }}</router-link></td> <td><human-date :date="playlist.modification_date"></human-date></td> <td>{{ playlist.tracks_count }}</td>