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.
|
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,
|
When working on the front-end, any end-user string should be marked as a translatable string,
|
||||||
with the proper context, as described below.
|
with the proper context, as described below.
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,8 @@ class DomainAdmin(admin.ModelAdmin):
|
||||||
@admin.register(models.Activity)
|
@admin.register(models.Activity)
|
||||||
class ActivityAdmin(admin.ModelAdmin):
|
class ActivityAdmin(admin.ModelAdmin):
|
||||||
list_display = ["type", "fid", "url", "actor", "creation_date"]
|
list_display = ["type", "fid", "url", "actor", "creation_date"]
|
||||||
search_fields = ["payload", "fid", "url", "actor__domain"]
|
search_fields = ["payload", "fid", "url", "actor__domain__name"]
|
||||||
list_filter = ["type", "actor__domain"]
|
list_filter = ["type", "actor__domain__name"]
|
||||||
actions = [redeliver_activities]
|
actions = [redeliver_activities]
|
||||||
list_select_related = True
|
list_select_related = True
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class ActorAdmin(admin.ModelAdmin):
|
||||||
"creation_date",
|
"creation_date",
|
||||||
"last_fetch_date",
|
"last_fetch_date",
|
||||||
]
|
]
|
||||||
search_fields = ["fid", "domain", "preferred_username"]
|
search_fields = ["fid", "domain__name", "preferred_username"]
|
||||||
list_filter = ["type"]
|
list_filter = ["type"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -859,7 +859,7 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
from_activity = self.context.get("activity")
|
from_activity = self.context.get("activity")
|
||||||
if from_activity:
|
if from_activity:
|
||||||
metadata["from_activity_id"] = from_activity.pk
|
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
|
return track
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,9 @@ from . import serializers
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
if album.cover and not replace:
|
||||||
return
|
return
|
||||||
if cover_data:
|
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)
|
cover = get_cover_from_fs(path)
|
||||||
if cover:
|
if cover:
|
||||||
return album.get_image(data=cover)
|
return album.get_image(data=cover)
|
||||||
if album.mbid:
|
if musicbrainz and album.mbid:
|
||||||
try:
|
try:
|
||||||
logger.info(
|
logger.info(
|
||||||
"[Album %s] Fetching cover from musicbrainz release %s",
|
"[Album %s] Fetching cover from musicbrainz release %s",
|
||||||
|
@ -179,8 +181,8 @@ def process_upload(upload):
|
||||||
import_metadata = upload.import_metadata or {}
|
import_metadata = upload.import_metadata or {}
|
||||||
old_status = upload.import_status
|
old_status = upload.import_status
|
||||||
audio_file = upload.get_audio_file()
|
audio_file = upload.get_audio_file()
|
||||||
|
additional_data = {}
|
||||||
try:
|
try:
|
||||||
additional_data = {}
|
|
||||||
if not audio_file:
|
if not audio_file:
|
||||||
# we can only rely on user proveded data
|
# we can only rely on user proveded data
|
||||||
final_metadata = import_metadata
|
final_metadata = import_metadata
|
||||||
|
@ -241,6 +243,15 @@ def process_upload(upload):
|
||||||
"bitrate",
|
"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(
|
broadcast = getter(
|
||||||
import_metadata, "funkwhale", "config", "broadcast", default=True
|
import_metadata, "funkwhale", "config", "broadcast", default=True
|
||||||
)
|
)
|
||||||
|
@ -369,7 +380,18 @@ def sort_candidates(candidates, important_fields):
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@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")
|
track_uuid = getter(data, "funkwhale", "track", "uuid")
|
||||||
|
|
||||||
if track_uuid:
|
if track_uuid:
|
||||||
|
@ -380,12 +402,6 @@ def get_track_from_import_metadata(data):
|
||||||
except models.Track.DoesNotExist:
|
except models.Track.DoesNotExist:
|
||||||
raise UploadImportError(code="track_uuid_not_found")
|
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
|
return track
|
||||||
|
|
||||||
from_activity_id = data.get("from_activity_id", None)
|
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(
|
album = get_best_candidate_or_create(
|
||||||
models.Album, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
models.Album, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
||||||
)[0]
|
)[0]
|
||||||
if not album.cover:
|
|
||||||
update_album_cover(
|
|
||||||
album, source=data.get("upload_source"), cover_data=data.get("cover_data")
|
|
||||||
)
|
|
||||||
|
|
||||||
# get / create track
|
# get / create track
|
||||||
track_title = data["title"]
|
track_title = data["title"]
|
||||||
|
|
|
@ -70,6 +70,7 @@ def get_track_data(album, track, upload):
|
||||||
"album": album.title,
|
"album": album.title,
|
||||||
"artist": album.artist.name,
|
"artist": album.artist.name,
|
||||||
"track": track.position or 1,
|
"track": track.position or 1,
|
||||||
|
"discNumber": track.disc_number or 1,
|
||||||
"contentType": upload.mimetype,
|
"contentType": upload.mimetype,
|
||||||
"suffix": upload.extension or "",
|
"suffix": upload.extension or "",
|
||||||
"duration": upload.duration or 0,
|
"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"))
|
r_mock.get(metadata["cover_data"]["url"], body=io.BytesIO(b"coucou"))
|
||||||
mocker.patch("funkwhale_api.music.metadata.Metadata.all", return_value=metadata)
|
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.title == metadata["title"]
|
||||||
assert track.fid == metadata["fid"]
|
assert track.fid == metadata["fid"]
|
||||||
|
@ -183,7 +183,9 @@ def test_sort_candidates(factories):
|
||||||
|
|
||||||
def test_upload_import(now, factories, temp_signal, mocker):
|
def test_upload_import(now, factories, temp_signal, mocker):
|
||||||
outbox = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
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"](
|
upload = factories["music.Upload"](
|
||||||
track=None, import_metadata={"funkwhale": {"track": {"uuid": str(track.uuid)}}}
|
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.track == track
|
||||||
assert upload.import_status == "finished"
|
assert upload.import_status == "finished"
|
||||||
assert upload.import_date == now
|
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(
|
handler.assert_called_once_with(
|
||||||
upload=upload,
|
upload=upload,
|
||||||
old_status="pending",
|
old_status="pending",
|
||||||
|
|
|
@ -64,7 +64,7 @@ def test_get_artist_serializer(factories):
|
||||||
def test_get_album_serializer(factories):
|
def test_get_album_serializer(factories):
|
||||||
artist = factories["music.Artist"]()
|
artist = factories["music.Artist"]()
|
||||||
album = factories["music.Album"](artist=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)
|
upload = factories["music.Upload"](track=track, bitrate=42000, duration=43, size=44)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
|
@ -85,6 +85,7 @@ def test_get_album_serializer(factories):
|
||||||
"album": album.title,
|
"album": album.title,
|
||||||
"artist": artist.name,
|
"artist": artist.name,
|
||||||
"track": track.position,
|
"track": track.position,
|
||||||
|
"discNumber": track.disc_number,
|
||||||
"year": track.album.release_date.year,
|
"year": track.album.release_date.year,
|
||||||
"contentType": upload.mimetype,
|
"contentType": upload.mimetype,
|
||||||
"suffix": upload.extension or "",
|
"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>
|
</Proxy>
|
||||||
|
|
||||||
# Activating WebSockets
|
# 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 "/">
|
<Location "/">
|
||||||
# similar to nginx 'client_max_body_size 100M;'
|
# 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/
|
ProxyPassReverse ${funkwhale-api}/.well-known/
|
||||||
</Location>
|
</Location>
|
||||||
|
|
||||||
ProxyPass "/front" "!"
|
<Location "/front">
|
||||||
|
ProxyPass "!"
|
||||||
|
</Location>
|
||||||
Alias /front /srv/funkwhale/front/dist
|
Alias /front /srv/funkwhale/front/dist
|
||||||
|
|
||||||
ProxyPass "/media" "!"
|
<Location "/media">
|
||||||
|
ProxyPass "!"
|
||||||
|
</Location>
|
||||||
Alias /media /srv/funkwhale/data/media
|
Alias /media /srv/funkwhale/data/media
|
||||||
|
|
||||||
ProxyPass "/staticfiles" "!"
|
<Location "/staticfiles">
|
||||||
|
ProxyPass "!"
|
||||||
|
</Location>
|
||||||
Alias /staticfiles /srv/funkwhale/data/static
|
Alias /staticfiles /srv/funkwhale/data/static
|
||||||
|
|
||||||
# Setting appropriate access levels to serve frontend
|
# 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}
|
command: python /app/manage.py runserver 0.0.0.0:${FUNKWHALE_API_PORT-5000}
|
||||||
volumes:
|
volumes:
|
||||||
- ./api:/app
|
- ./api:/app
|
||||||
- "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
|
- "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro"
|
||||||
environment:
|
environment:
|
||||||
- "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
|
- "FUNKWHALE_HOSTNAME=${FUNKWHALE_HOSTNAME-localhost}"
|
||||||
- "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
|
- "FUNKWHALE_HOSTNAME_SUFFIX=funkwhale.test"
|
||||||
|
@ -87,7 +87,7 @@ services:
|
||||||
- "CACHE_URL=redis://redis:6379/0"
|
- "CACHE_URL=redis://redis:6379/0"
|
||||||
volumes:
|
volumes:
|
||||||
- ./api:/app
|
- ./api:/app
|
||||||
- "${MUSIC_DIRECTORY_PATH-./data/music}:/music:ro"
|
- "${MUSIC_DIRECTORY_SERVE_PATH-./data/music}:/music:ro"
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
nginx:
|
nginx:
|
||||||
|
@ -112,7 +112,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/nginx/conf.dev:/etc/nginx/nginx.conf.template:ro
|
- ./docker/nginx/conf.dev:/etc/nginx/nginx.conf.template:ro
|
||||||
- ./docker/nginx/entrypoint.sh:/entrypoint.sh: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
|
- ./deploy/funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro
|
||||||
- "${MEDIA_ROOT-./api/funkwhale_api/media}:/protected/media:ro"
|
- "${MEDIA_ROOT-./api/funkwhale_api/media}:/protected/media:ro"
|
||||||
networks:
|
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::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
troubleshooting
|
troubleshooting
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Uploading and removing content on Funkwhale
|
Uploading Content To Funkwhale
|
||||||
===========================================
|
==============================
|
||||||
|
|
||||||
To upload content to any Funkwhale instance, you need:
|
To upload content to any Funkwhale instance, you need:
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ can vary depending on server load.
|
||||||
Removing files
|
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.
|
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".
|
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">
|
<img class="ui mini image" v-else src="../assets/audio/default-cover.png">
|
||||||
</td>
|
</td>
|
||||||
<td colspan="4">
|
<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 />
|
<strong>{{ track.title }}</strong><br />
|
||||||
{{ track.artist.name }}
|
<span>
|
||||||
|
{{ track.artist.name }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span>
|
<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>
|
<span v-translate="{artist: album.artist.name}" translate-context="Content/Album/Card" :translate-params="{artist: album.artist.name}">By %{ artist }</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</span><span class="time" v-if="album.release_date">– {{ album.release_date | year }}</span>
|
</span><span class="time" v-if="album.release_date">– {{ album.release_date | year }}</span>
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="content-cell" colspan="5">
|
<td class="content-cell" colspan="5">
|
||||||
<track-favorite-icon :track="track"></track-favorite-icon>
|
<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">
|
<template v-if="track.position">
|
||||||
{{ track.position }}.
|
{{ track.position }}.
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
||||||
</td>
|
</td>
|
||||||
<td colspan="4">
|
<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>
|
<strong>{{ album.title }}</strong>
|
||||||
</router-link><br />
|
</router-link><br />
|
||||||
{{ album.tracks_count }} tracks
|
{{ album.tracks_count }} tracks
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
|
||||||
</td>
|
</td>
|
||||||
<td colspan="6">
|
<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">
|
<template v-if="displayPosition && track.position">
|
||||||
{{ track.position }}.
|
{{ track.position }}.
|
||||||
</template>
|
</template>
|
||||||
|
@ -16,21 +16,21 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="4">
|
<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 }}
|
{{ track.artist.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<template v-else>
|
<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 }}
|
{{ albumArtist.name }}
|
||||||
</router-link>
|
</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 }}
|
{{ track.artist.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="4">
|
<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 }}
|
{{ track.album.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
class="ui icon basic small button"
|
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>
|
:to="{name: 'library.playlists.detail', params: {id: playlist.id }, query: {mode: 'edit'}}"><i class="ui pencil icon"></i></router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td :title="playlist.name">
|
||||||
<router-link :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">{{ playlist.name }}</router-link></td>
|
<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><human-date :date="playlist.modification_date"></human-date></td>
|
||||||
<td>{{ playlist.tracks_count }}</td>
|
<td>{{ playlist.tracks_count }}</td>
|
||||||
|
|
Loading…
Reference in New Issue