Merge branch 'master' into develop
This commit is contained in:
commit
9a162c57ca
338
CONTRIBUTING.rst
338
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.
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 "",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Ensure cover art from uploaded files is picked up properly on existing albums (#757)
|
|
@ -0,0 +1 @@
|
|||
Fixed broken sample apache configuration (#764)
|
|
@ -0,0 +1 @@
|
|||
Include disc number in Subsonic responses (#765)
|
|
@ -0,0 +1 @@
|
|||
Added title on hover for truncated content (#766)
|
|
@ -0,0 +1 @@
|
|||
Fixed broken Activity and Actor modules in django admin (#767)
|
|
@ -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
|
||||
|
|
6
dev.yml
6
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:
|
||||
|
|
|
@ -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.
|
|
@ -34,5 +34,5 @@ Troubleshooting Issues
|
|||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
troubleshooting
|
||||
|
|
|
@ -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".
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue