Compare commits

...

8 Commits

Author SHA1 Message Date
FunTranslater 5f5b185cfa Translated using Weblate (Russian)
Currently translated at 95.1% (2171 of 2282 strings)

Translation: Funkwhale/Funkwhale Web
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/front/ru/
2025-07-10 14:34:13 +00:00
FunTranslater 2c0f0fd86b Added translation using Weblate (Ukrainian) 2025-07-10 14:19:27 +00:00
petitminion bd12cc389e fix(front):make soundCache reactive to allow player to get track metadata when the queue only contains one track 2025-06-18 11:36:56 +00:00
JuniorJPDJ aaa4e6eb92 fix(api): allow importing single files using fs-import API endpoint 2025-06-13 11:15:46 +00:00
ArneBo e1bc3d2474 fix(front): syntax and performance improvements in playlist detail 2025-06-12 15:29:09 +02:00
Petitminion f8bfbb4434 fix(schema):update schema and documentation for release 2025-06-11 12:16:33 +02:00
Petitminion 6bc858986b fix(pylint):add default FUNKWHALE_URL var 2025-06-11 10:25:32 +02:00
Petitminion df34dcb452 fix(backend):pylint django setting variable not properly set 2025-06-11 10:04:31 +02:00
14 changed files with 4845 additions and 13 deletions

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: Funkwhale API title: Funkwhale API
version: 2.0.0a1 version: 2.0.0a2
description: | description: |
# Funkwhale API # Funkwhale API

View File

@ -1,3 +1,5 @@
import os
import pathlib
import urllib.parse import urllib.parse
from django import urls from django import urls
@ -903,13 +905,17 @@ class FSImportSerializer(serializers.Serializer):
prune = serializers.BooleanField(required=False, default=True) prune = serializers.BooleanField(required=False, default=True)
outbox = serializers.BooleanField(required=False, default=False) outbox = serializers.BooleanField(required=False, default=False)
broadcast = serializers.BooleanField(required=False, default=False) broadcast = serializers.BooleanField(required=False, default=False)
replace = serializers.BooleanField(required=False, default=False)
batch_size = serializers.IntegerField(required=False, default=1000) batch_size = serializers.IntegerField(required=False, default=1000)
verbosity = serializers.IntegerField(required=False, default=1) verbosity = serializers.IntegerField(required=False, default=1)
def validate_path(self, value): def validate_path(self, value):
try: try:
utils.browse_dir(settings.MUSIC_DIRECTORY_PATH, value) utils.browse_dir(settings.MUSIC_DIRECTORY_PATH, value)
except (NotADirectoryError, FileNotFoundError, ValueError): except NotADirectoryError:
if not os.path.isfile(pathlib.Path(settings.MUSIC_DIRECTORY_PATH) / value):
raise serializers.ValidationError("Invalid path")
except (FileNotFoundError, ValueError):
raise serializers.ValidationError("Invalid path") raise serializers.ValidationError("Invalid path")
return value return value

View File

@ -1209,6 +1209,7 @@ def fs_import(
prune=True, prune=True,
outbox=False, outbox=False,
broadcast=False, broadcast=False,
replace=False,
batch_size=1000, batch_size=1000,
verbosity=1, verbosity=1,
): ):
@ -1229,7 +1230,7 @@ def fs_import(
"batch_size": batch_size, "batch_size": batch_size,
"async_": False, "async_": False,
"prune": prune, "prune": prune,
"replace": False, "replace": replace,
"verbosity": verbosity, "verbosity": verbosity,
"exit_on_failure": False, "exit_on_failure": False,
"outbox": outbox, "outbox": outbox,

View File

@ -386,6 +386,7 @@ class LibraryViewSet(
prune=serializer.validated_data["prune"], prune=serializer.validated_data["prune"],
outbox=serializer.validated_data["outbox"], outbox=serializer.validated_data["outbox"],
broadcast=serializer.validated_data["broadcast"], broadcast=serializer.validated_data["broadcast"],
replace=serializer.validated_data["replace"],
batch_size=serializer.validated_data["batch_size"], batch_size=serializer.validated_data["batch_size"],
verbosity=serializer.validated_data["verbosity"], verbosity=serializer.validated_data["verbosity"],
) )

View File

@ -143,7 +143,7 @@ class PlaylistViewSet(
# Apply pagination # Apply pagination
paginator = PageNumberPagination() paginator = PageNumberPagination()
paginator.page_size = 100 # Set the page size (number of items per page) paginator.page_size = 50 # Set the page size (number of items per page)
paginated_plts = paginator.paginate_queryset(plts, request) paginated_plts = paginator.paginate_queryset(plts, request)
# Serialize the paginated data # Serialize the paginated data

View File

@ -135,7 +135,8 @@ build-backend = "poetry.core.masonry.api"
[tool.pylint.master] [tool.pylint.master]
load-plugins = ["pylint_django"] load-plugins = ["pylint_django"]
django-settings-module = "config.settings.testing" django-settings-module = "config.settings.local"
init-hook = 'import os; os.environ.setdefault("FUNKWHALE_URL", "https://test.federation")'
[tool.pylint.messages_control] [tool.pylint.messages_control]
disable = [ disable = [

View File

@ -1528,6 +1528,7 @@ def test_fs_import_post(
prune=True, prune=True,
outbox=False, outbox=False,
broadcast=False, broadcast=False,
replace=False,
batch_size=1000, batch_size=1000,
verbosity=1, verbosity=1,
) )

View File

@ -0,0 +1 @@
Allow importing single files using fs-import API endpoint

View File

@ -58,6 +58,8 @@ Once we're ready to release a new version of the software, we can use the follow
7. Update the next release version 7. Update the next release version
```sh ```sh
docker compose build api
docker compose run --rm api funkwhale-manage spectacular > ./api/funkwhale_api/common/schema.yml
cd api cd api
poetry version "$NEXT_RELEASE" poetry version "$NEXT_RELEASE"
cd .. cd ..

View File

@ -45,7 +45,7 @@ interface Props {
paginateResults?: boolean paginateResults?: boolean
total?: number total?: number
page?: number page?: number
paginateBy?: number, paginateBy?: number
unique?: boolean unique?: boolean
} }

View File

@ -27,6 +27,14 @@ const soundCache = new LRUCache<number, Sound>({
dispose: (sound) => sound.dispose() dispose: (sound) => sound.dispose()
}) })
// used to make soundCache reactive
const soundCacheVersion = ref(0)
function setSoundCache(trackId: number, sound: Sound) {
soundCache.set(trackId, sound)
soundCacheVersion.value++ // bump to trigger reactivity
}
const currentTrack = ref<QueueTrack>() const currentTrack = ref<QueueTrack>()
export const fetchTrackSources = async (id: number): Promise<QueueTrackSource[]> => { export const fetchTrackSources = async (id: number): Promise<QueueTrackSource[]> => {
@ -139,7 +147,7 @@ export const useTracks = createGlobalState(() => {
} }
// Add track to the sound cache and remove from the promise cache // Add track to the sound cache and remove from the promise cache
soundCache.set(track.id, sound) setSoundCache(track.id, sound)
soundPromises.delete(track.id) soundPromises.delete(track.id)
return sound return sound
@ -224,7 +232,12 @@ export const useTracks = createGlobalState(() => {
}) })
}) })
const currentSound = computed(() => soundCache.get(currentTrack.value?.id ?? -1)) const currentSound = computed(() => {
soundCacheVersion.value //trigger reactivity
const trackId = currentTrack.value?.id ?? -1
const sound = soundCache.get(trackId)
return sound
})
const clearCache = () => { const clearCache = () => {
return soundCache.clear() return soundCache.clear()

View File

@ -4611,5 +4611,8 @@
"title": "Радио" "title": "Радио"
} }
} }
},
"vui": {
"radio": "Радио"
} }
} }

4804
front/src/locales/uk.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -96,20 +96,19 @@ const loadMoreTracks = async () => {
if (nextPage.value) { if (nextPage.value) {
isLoadingMoreTracks.value = true; // Set loading state for the button isLoadingMoreTracks.value = true; // Set loading state for the button
try { try {
const response = await axios.get(nextPage.value); const response = await axios.get(nextPage.value)
// Append new tracks to the existing list // Append new tracks to the existing list
fullPlaylistTracks.value = [...fullPlaylistTracks.value, ...response.data.results]; fullPlaylistTracks.value = [...fullPlaylistTracks.value, ...response.data.results]
// Update pagination metadata // Update pagination metadata
nextPage.value = response.data.next; nextPage.value = response.data.next
} catch (error) { } catch (error) {
useErrorHandler(error as Error) useErrorHandler(error as Error)
} finally { } finally {
isLoadingMoreTracks.value = false; // Reset loading state isLoadingMoreTracks.value = false; // Reset loading state
} }
} }
}; }
fetchData() fetchData()