Merge branch 'master' into develop
This commit is contained in:
commit
88c6c2bdbc
|
@ -218,3 +218,12 @@ class AlbumFilter(
|
||||||
def filter_playable(self, queryset, name, value):
|
def filter_playable(self, queryset, name, value):
|
||||||
actor = utils.get_actor_from_request(self.request)
|
actor = utils.get_actor_from_request(self.request)
|
||||||
return queryset.playable_by(actor, value)
|
return queryset.playable_by(actor, value)
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryFilter(filters.FilterSet):
|
||||||
|
q = fields.SearchFilter(search_fields=["name"],)
|
||||||
|
scope = common_filters.ActorScopeFilter(actor_field="actor", distinct=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Library
|
||||||
|
fields = ["privacy_level", "q", "scope"]
|
||||||
|
|
|
@ -27,7 +27,8 @@ def crawl_dir(dir, extensions, recursive=True, ignored=[]):
|
||||||
if os.path.isfile(dir):
|
if os.path.isfile(dir):
|
||||||
yield dir
|
yield dir
|
||||||
return
|
return
|
||||||
with os.scandir(dir) as scanner:
|
try:
|
||||||
|
scanner = os.scandir(dir)
|
||||||
for entry in scanner:
|
for entry in scanner:
|
||||||
if entry.is_file():
|
if entry.is_file():
|
||||||
for e in extensions:
|
for e in extensions:
|
||||||
|
@ -38,6 +39,9 @@ def crawl_dir(dir, extensions, recursive=True, ignored=[]):
|
||||||
yield from crawl_dir(
|
yield from crawl_dir(
|
||||||
entry, extensions, recursive=recursive, ignored=ignored
|
entry, extensions, recursive=recursive, ignored=ignored
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
if hasattr(scanner, "close"):
|
||||||
|
scanner.close()
|
||||||
|
|
||||||
|
|
||||||
def batch(iterable, n=1):
|
def batch(iterable, n=1):
|
||||||
|
|
|
@ -273,6 +273,7 @@ class LibraryViewSet(
|
||||||
oauth_permissions.ScopePermission,
|
oauth_permissions.ScopePermission,
|
||||||
common_permissions.OwnerPermission,
|
common_permissions.OwnerPermission,
|
||||||
]
|
]
|
||||||
|
filterset_class = filters.LibraryFilter
|
||||||
required_scope = "libraries"
|
required_scope = "libraries"
|
||||||
anonymous_policy = "setting"
|
anonymous_policy = "setting"
|
||||||
owner_field = "actor.user"
|
owner_field = "actor.user"
|
||||||
|
@ -282,8 +283,12 @@ class LibraryViewSet(
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
# allow retrieving a single library by uuid if request.user isn't
|
# allow retrieving a single library by uuid if request.user isn't
|
||||||
# the owner. Any other get should be from the owner only
|
# the owner. Any other get should be from the owner only
|
||||||
if self.action != "retrieve":
|
if self.action not in ["retrieve", "list"]:
|
||||||
qs = qs.filter(actor=self.request.user.actor)
|
qs = qs.filter(actor=self.request.user.actor)
|
||||||
|
if self.action == "list":
|
||||||
|
actor = utils.get_actor_from_request(self.request)
|
||||||
|
qs = qs.viewable_by(actor)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
|
|
Binary file not shown.
|
@ -631,10 +631,10 @@ def test_user_can_create_library(factories, logged_in_api_client):
|
||||||
def test_user_can_list_their_library(factories, logged_in_api_client):
|
def test_user_can_list_their_library(factories, logged_in_api_client):
|
||||||
actor = logged_in_api_client.user.create_actor()
|
actor = logged_in_api_client.user.create_actor()
|
||||||
library = factories["music.Library"](actor=actor)
|
library = factories["music.Library"](actor=actor)
|
||||||
factories["music.Library"]()
|
factories["music.Library"](privacy_level="everyone")
|
||||||
|
|
||||||
url = reverse("api:v1:libraries-list")
|
url = reverse("api:v1:libraries-list")
|
||||||
response = logged_in_api_client.get(url)
|
response = logged_in_api_client.get(url, {"scope": "me"})
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.data["count"] == 1
|
assert response.data["count"] == 1
|
||||||
|
@ -651,6 +651,19 @@ def test_user_can_retrieve_another_user_library(factories, logged_in_api_client)
|
||||||
assert response.data["uuid"] == str(library.uuid)
|
assert response.data["uuid"] == str(library.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_can_list_public_libraries(factories, api_client, preferences):
|
||||||
|
preferences["common__api_authentication_required"] = False
|
||||||
|
library = factories["music.Library"](privacy_level="everyone")
|
||||||
|
factories["music.Library"](privacy_level="me")
|
||||||
|
|
||||||
|
url = reverse("api:v1:libraries-list")
|
||||||
|
response = api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data["count"] == 1
|
||||||
|
assert response.data["results"][0]["uuid"] == str(library.uuid)
|
||||||
|
|
||||||
|
|
||||||
def test_library_list_excludes_channel_library(factories, logged_in_api_client):
|
def test_library_list_excludes_channel_library(factories, logged_in_api_client):
|
||||||
actor = logged_in_api_client.user.create_actor()
|
actor = logged_in_api_client.user.create_actor()
|
||||||
factories["audio.Channel"](attributed_to=actor)
|
factories["audio.Channel"](attributed_to=actor)
|
||||||
|
|
|
@ -352,3 +352,17 @@ def test_handle_modified_update_existing_path_if_found_and_attributed_to(
|
||||||
event=event, stdout=stdout, library=library, in_place=True,
|
event=event, stdout=stdout, library=library, in_place=True,
|
||||||
)
|
)
|
||||||
update_track_metadata.assert_not_called()
|
update_track_metadata.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_files(factories, capsys):
|
||||||
|
# smoke test to ensure the command run properly
|
||||||
|
library = factories["music.Library"](actor__local=True)
|
||||||
|
call_command(
|
||||||
|
"import_files", str(library.uuid), DATA_DIR, interactive=False, recursive=True
|
||||||
|
)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
|
imported = library.uploads.filter(import_status="finished").count()
|
||||||
|
assert imported > 0
|
||||||
|
assert "Successfully imported {} new tracks".format(imported) in captured.out
|
||||||
|
assert "For details, please refer to import reference" in captured.out
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed recursive CLI importing crashing under Python 3.5 (#1148, #1147)
|
|
@ -0,0 +1 @@
|
||||||
|
Added new channels widget on pod landing page (#1113)
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed a wording issue on artist channel page (#1117)
|
|
@ -0,0 +1 @@
|
||||||
|
Updated the /api/v1/libraries endpoint to support listing public libraries from other users/pods (#1151)
|
|
@ -18,3 +18,16 @@ Because of this change, existing thumbnails will not load, and you will need to:
|
||||||
2. run ``python manage.py fw media generate-thumbnails`` to regenerate thumbnails with the enhanced quality
|
2. run ``python manage.py fw media generate-thumbnails`` to regenerate thumbnails with the enhanced quality
|
||||||
|
|
||||||
If you don't want to regenerate thumbnails, you can keep the old ones by adding ``THUMBNAIL_JPEG_RESIZE_QUALITY=70`` to your .env file.
|
If you don't want to regenerate thumbnails, you can keep the old ones by adding ``THUMBNAIL_JPEG_RESIZE_QUALITY=70`` to your .env file.
|
||||||
|
|
||||||
|
Small API breaking change in ``/api/v1/libraries``
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
To allow easier crawling of public libraries on a pod,we had to make a slight breaking change
|
||||||
|
to the behaviour of ``GET /api/v1/libraries``.
|
||||||
|
|
||||||
|
Before, it returned only libraries owned by the current user.
|
||||||
|
|
||||||
|
Now, it returns all the accessible libraries (including ones from other users and pods).
|
||||||
|
|
||||||
|
If you are consuming the API via a third-party client and need to retrieve your libraries,
|
||||||
|
use the ``scope`` parameter, like this: ``GET /api/v1/libraries?scope=me``
|
||||||
|
|
|
@ -173,6 +173,12 @@
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider"></div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</album-widget>
|
</album-widget>
|
||||||
|
<div class="ui hidden section divider"></div>
|
||||||
|
<h3 class="ui header" >
|
||||||
|
<translate translate-context="*/*/*">New channels</translate>
|
||||||
|
</h3>
|
||||||
|
<channels-widget :show-modification-date="true" :limit="10" :filters="{ordering: '-creation_date', external: 'false'}"></channels-widget>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
@ -183,6 +189,7 @@ import _ from '@/lodash'
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
import showdown from 'showdown'
|
import showdown from 'showdown'
|
||||||
import AlbumWidget from "@/components/audio/album/Widget"
|
import AlbumWidget from "@/components/audio/album/Widget"
|
||||||
|
import ChannelsWidget from "@/components/audio/ChannelsWidget"
|
||||||
import LoginForm from "@/components/auth/LoginForm"
|
import LoginForm from "@/components/auth/LoginForm"
|
||||||
import SignupForm from "@/components/auth/SignupForm"
|
import SignupForm from "@/components/auth/SignupForm"
|
||||||
import {humanSize } from '@/filters'
|
import {humanSize } from '@/filters'
|
||||||
|
@ -190,6 +197,7 @@ import {humanSize } from '@/filters'
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AlbumWidget,
|
AlbumWidget,
|
||||||
|
ChannelsWidget,
|
||||||
LoginForm,
|
LoginForm,
|
||||||
SignupForm,
|
SignupForm,
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,11 +18,14 @@
|
||||||
<template v-if="totalTracks > 0">
|
<template v-if="totalTracks > 0">
|
||||||
<div class="ui hidden very small divider"></div>
|
<div class="ui hidden very small divider"></div>
|
||||||
<translate translate-context="Content/Channel/Paragraph"
|
<translate translate-context="Content/Channel/Paragraph"
|
||||||
|
key="1"
|
||||||
|
v-if="object.artist.content_category === 'podcast'"
|
||||||
translate-plural="%{ count } episodes"
|
translate-plural="%{ count } episodes"
|
||||||
:translate-n="totalTracks"
|
:translate-n="totalTracks"
|
||||||
:translate-params="{count: totalTracks}">
|
:translate-params="{count: totalTracks}">
|
||||||
%{ count } episode
|
%{ count } episode
|
||||||
</translate>
|
</translate>
|
||||||
|
<translate key="2" v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="object.attributed_to.full_username === $store.state.auth.fullUsername || $store.getters['channels/isSubscribed'](object.uuid)">
|
<template v-if="object.attributed_to.full_username === $store.state.auth.fullUsername || $store.getters['channels/isSubscribed'](object.uuid)">
|
||||||
· <translate translate-context="Content/Channel/Paragraph" translate-plural="%{ count } subscribers" :translate-n="object.subscriptions_count" :translate-params="{count: object.subscriptions_count}">%{ count } subscriber</translate>
|
· <translate translate-context="Content/Channel/Paragraph" translate-plural="%{ count } subscribers" :translate-n="object.subscriptions_count" :translate-params="{count: object.subscriptions_count}">%{ count } subscriber</translate>
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default {
|
||||||
fetch() {
|
fetch() {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
let self = this
|
let self = this
|
||||||
axios.get("libraries/").then(response => {
|
axios.get("libraries/", {params: {scope: 'me'}}).then(response => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.libraries = response.data.results
|
self.libraries = response.data.results
|
||||||
if (self.libraries.length === 0) {
|
if (self.libraries.length === 0) {
|
||||||
|
|
Loading…
Reference in New Issue