diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index 79b2d2c63872f2816316b2e711cb20037f0f630a..d69dd13a3d3b095cabbc5730b78a970c35c88adf 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -218,3 +218,12 @@ class AlbumFilter( def filter_playable(self, queryset, name, value): actor = utils.get_actor_from_request(self.request) 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"] diff --git a/api/funkwhale_api/music/management/commands/import_files.py b/api/funkwhale_api/music/management/commands/import_files.py index fab980510d77b045df80df803d0fa40642a520ac..ddc598d06fcf5dfa50ac1508c90e563c59cf6a0e 100644 --- a/api/funkwhale_api/music/management/commands/import_files.py +++ b/api/funkwhale_api/music/management/commands/import_files.py @@ -27,7 +27,8 @@ def crawl_dir(dir, extensions, recursive=True, ignored=[]): if os.path.isfile(dir): yield dir return - with os.scandir(dir) as scanner: + try: + scanner = os.scandir(dir) for entry in scanner: if entry.is_file(): for e in extensions: @@ -38,6 +39,9 @@ def crawl_dir(dir, extensions, recursive=True, ignored=[]): yield from crawl_dir( entry, extensions, recursive=recursive, ignored=ignored ) + finally: + if hasattr(scanner, "close"): + scanner.close() def batch(iterable, n=1): diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 93635c11b55be1b10e996ad62f281e43f41e6c2f..6c9f7e41c4754b8c2a40819ab02529965e6583bb 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -273,6 +273,7 @@ class LibraryViewSet( oauth_permissions.ScopePermission, common_permissions.OwnerPermission, ] + filterset_class = filters.LibraryFilter required_scope = "libraries" anonymous_policy = "setting" owner_field = "actor.user" @@ -282,8 +283,12 @@ class LibraryViewSet( qs = super().get_queryset() # allow retrieving a single library by uuid if request.user isn't # 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) + if self.action == "list": + actor = utils.get_actor_from_request(self.request) + qs = qs.viewable_by(actor) + return qs def perform_create(self, serializer): diff --git a/api/tests/files/nested/valid.ogg b/api/tests/files/nested/valid.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e1643848a07a548e55d53726d4e4fe168a8ab2cd Binary files /dev/null and b/api/tests/files/nested/valid.ogg differ diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py index 4f2b6226919739626ddb4d6d4baa2afec7727248..5e3c9c953be2d44a55a01d7d4a0c1d757f10f047 100644 --- a/api/tests/music/test_views.py +++ b/api/tests/music/test_views.py @@ -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): actor = logged_in_api_client.user.create_actor() library = factories["music.Library"](actor=actor) - factories["music.Library"]() + factories["music.Library"](privacy_level="everyone") 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.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) +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): actor = logged_in_api_client.user.create_actor() factories["audio.Channel"](attributed_to=actor) diff --git a/api/tests/test_import_audio_file.py b/api/tests/test_import_audio_file.py index 04c06a8f462fbdd7db3dfe4e31139f7edc5585c6..7ee1028c8fd7e331208474a7a871bd41931e1bb4 100644 --- a/api/tests/test_import_audio_file.py +++ b/api/tests/test_import_audio_file.py @@ -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, ) 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 diff --git a/changes/changelog.d/1048.bugfix b/changes/changelog.d/1048.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..0f1973444147ffad54d32a715f7fa5d9a07dda28 --- /dev/null +++ b/changes/changelog.d/1048.bugfix @@ -0,0 +1 @@ +Fixed recursive CLI importing crashing under Python 3.5 (#1148, #1147) diff --git a/changes/changelog.d/1113.enhancement b/changes/changelog.d/1113.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..979b33056dfd728c2b2ca9c1298962cfa25d7b2a --- /dev/null +++ b/changes/changelog.d/1113.enhancement @@ -0,0 +1 @@ +Added new channels widget on pod landing page (#1113) diff --git a/changes/changelog.d/1117.bugfix b/changes/changelog.d/1117.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..b2236ee70ae1d1186322e5f57d1d50a9d74d70d4 --- /dev/null +++ b/changes/changelog.d/1117.bugfix @@ -0,0 +1 @@ +Fixed a wording issue on artist channel page (#1117) diff --git a/changes/changelog.d/1151.enhancement b/changes/changelog.d/1151.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..c9b867a30208c3669a9d4b93571add7fc588e04b --- /dev/null +++ b/changes/changelog.d/1151.enhancement @@ -0,0 +1 @@ +Updated the /api/v1/libraries endpoint to support listing public libraries from other users/pods (#1151) diff --git a/changes/notes.rst b/changes/notes.rst index ee03756587bdb21f04c4abe9f5311a5df5143928..8d84dc02621ce2db8bd1b62ea5dac3300e0a510a 100644 --- a/changes/notes.rst +++ b/changes/notes.rst @@ -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 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`` diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue index 2d010bb5e26e110ae27a2ace6fa79e6e8a117fea..a872022f1c468ed0ac33c8cd0dc9cded7baeed04 100644 --- a/front/src/components/Home.vue +++ b/front/src/components/Home.vue @@ -173,6 +173,12 @@ <div class="ui hidden divider"></div> </router-link> </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> </main> </template> @@ -183,6 +189,7 @@ import _ from '@/lodash' import {mapState} from 'vuex' import showdown from 'showdown' import AlbumWidget from "@/components/audio/album/Widget" +import ChannelsWidget from "@/components/audio/ChannelsWidget" import LoginForm from "@/components/auth/LoginForm" import SignupForm from "@/components/auth/SignupForm" import {humanSize } from '@/filters' @@ -190,6 +197,7 @@ import {humanSize } from '@/filters' export default { components: { AlbumWidget, + ChannelsWidget, LoginForm, SignupForm, }, diff --git a/front/src/views/channels/DetailBase.vue b/front/src/views/channels/DetailBase.vue index 0aaa735be7de807a64fd646ba065866d38266839..f838b0e8395e9b8af8060f7159a4a026e6ba53e8 100644 --- a/front/src/views/channels/DetailBase.vue +++ b/front/src/views/channels/DetailBase.vue @@ -18,11 +18,14 @@ <template v-if="totalTracks > 0"> <div class="ui hidden very small divider"></div> <translate translate-context="Content/Channel/Paragraph" + key="1" + v-if="object.artist.content_category === 'podcast'" translate-plural="%{ count } episodes" :translate-n="totalTracks" :translate-params="{count: totalTracks}"> %{ count } episode </translate> + <translate key="2" v-else translate-context="*/*/*" :translate-params="{count: totalTracks}" :translate-n="totalTracks" translate-plural="%{ count } tracks">%{ count } track</translate> </template> <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> diff --git a/front/src/views/content/libraries/Home.vue b/front/src/views/content/libraries/Home.vue index aeb61e9b2dddd555193e7a22269d2b583d1460c1..9a491bf54743ae2646b8de970baa2f09c52bb2c2 100644 --- a/front/src/views/content/libraries/Home.vue +++ b/front/src/views/content/libraries/Home.vue @@ -53,7 +53,7 @@ export default { fetch() { this.isLoading = true let self = this - axios.get("libraries/").then(response => { + axios.get("libraries/", {params: {scope: 'me'}}).then(response => { self.isLoading = false self.libraries = response.data.results if (self.libraries.length === 0) {