Merge branch '223-management-interface' into 'develop'
Resolve "Add a management interface for artists/albums/tracks" Closes #223 and #241 See merge request funkwhale/funkwhale!216
This commit is contained in:
commit
218a92547e
|
@ -38,6 +38,10 @@ v1_patterns += [
|
||||||
include(
|
include(
|
||||||
('funkwhale_api.instance.urls', 'instance'),
|
('funkwhale_api.instance.urls', 'instance'),
|
||||||
namespace='instance')),
|
namespace='instance')),
|
||||||
|
url(r'^manage/',
|
||||||
|
include(
|
||||||
|
('funkwhale_api.manage.urls', 'manage'),
|
||||||
|
namespace='manage')),
|
||||||
url(r'^federation/',
|
url(r'^federation/',
|
||||||
include(
|
include(
|
||||||
('funkwhale_api.federation.api_urls', 'federation'),
|
('funkwhale_api.federation.api_urls', 'federation'),
|
||||||
|
|
|
@ -97,6 +97,7 @@ THIRD_PARTY_APPS = (
|
||||||
'dynamic_preferences',
|
'dynamic_preferences',
|
||||||
'django_filters',
|
'django_filters',
|
||||||
'cacheops',
|
'cacheops',
|
||||||
|
'django_cleanup',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,9 @@ class ActionSerializer(serializers.Serializer):
|
||||||
filters = serializers.DictField(required=False)
|
filters = serializers.DictField(required=False)
|
||||||
actions = None
|
actions = None
|
||||||
filterset_class = None
|
filterset_class = None
|
||||||
|
# those are actions identifier where we don't want to allow the "all"
|
||||||
|
# selector because it's to dangerous. Like object deletion.
|
||||||
|
dangerous_actions = []
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.queryset = kwargs.pop('queryset')
|
self.queryset = kwargs.pop('queryset')
|
||||||
|
@ -49,6 +52,10 @@ class ActionSerializer(serializers.Serializer):
|
||||||
'list of identifiers or the string "all".'.format(value))
|
'list of identifiers or the string "all".'.format(value))
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
dangerous = data['action'] in self.dangerous_actions
|
||||||
|
if dangerous and self.initial_data['objects'] == 'all':
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
'This action is to dangerous to be applied to all objects')
|
||||||
if self.filterset_class and 'filters' in data:
|
if self.filterset_class and 'filters' in data:
|
||||||
qs_filterset = self.filterset_class(
|
qs_filterset = self.filterset_class(
|
||||||
data['filters'], queryset=data['objects'])
|
data['filters'], queryset=data['objects'])
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
"""
|
||||||
|
App that includes all views/serializers and stuff for management API
|
||||||
|
"""
|
|
@ -0,0 +1,25 @@
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
from django_filters import rest_framework as filters
|
||||||
|
|
||||||
|
from funkwhale_api.common import fields
|
||||||
|
from funkwhale_api.music import models as music_models
|
||||||
|
|
||||||
|
|
||||||
|
class ManageTrackFileFilterSet(filters.FilterSet):
|
||||||
|
q = fields.SearchFilter(search_fields=[
|
||||||
|
'track__title',
|
||||||
|
'track__album__title',
|
||||||
|
'track__artist__name',
|
||||||
|
'source',
|
||||||
|
])
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = music_models.TrackFile
|
||||||
|
fields = [
|
||||||
|
'q',
|
||||||
|
'track__album',
|
||||||
|
'track__artist',
|
||||||
|
'track',
|
||||||
|
'library_track'
|
||||||
|
]
|
|
@ -0,0 +1,82 @@
|
||||||
|
from django.db import transaction
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from funkwhale_api.common import serializers as common_serializers
|
||||||
|
from funkwhale_api.music import models as music_models
|
||||||
|
|
||||||
|
from . import filters
|
||||||
|
|
||||||
|
|
||||||
|
class ManageTrackFileArtistSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = music_models.Artist
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'creation_date',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ManageTrackFileAlbumSerializer(serializers.ModelSerializer):
|
||||||
|
artist = ManageTrackFileArtistSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = music_models.Album
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'title',
|
||||||
|
'artist',
|
||||||
|
'release_date',
|
||||||
|
'cover',
|
||||||
|
'creation_date',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ManageTrackFileTrackSerializer(serializers.ModelSerializer):
|
||||||
|
artist = ManageTrackFileArtistSerializer()
|
||||||
|
album = ManageTrackFileAlbumSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = music_models.Track
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'mbid',
|
||||||
|
'title',
|
||||||
|
'album',
|
||||||
|
'artist',
|
||||||
|
'creation_date',
|
||||||
|
'position',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ManageTrackFileSerializer(serializers.ModelSerializer):
|
||||||
|
track = ManageTrackFileTrackSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = music_models.TrackFile
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'path',
|
||||||
|
'source',
|
||||||
|
'filename',
|
||||||
|
'mimetype',
|
||||||
|
'track',
|
||||||
|
'duration',
|
||||||
|
'mimetype',
|
||||||
|
'bitrate',
|
||||||
|
'size',
|
||||||
|
'path',
|
||||||
|
'library_track',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ManageTrackFileActionSerializer(common_serializers.ActionSerializer):
|
||||||
|
actions = ['delete']
|
||||||
|
dangerous_actions = ['delete']
|
||||||
|
filterset_class = filters.ManageTrackFileFilterSet
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def handle_delete(self, objects):
|
||||||
|
return objects.delete()
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.conf.urls import include, url
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
from rest_framework import routers
|
||||||
|
library_router = routers.SimpleRouter()
|
||||||
|
library_router.register(r'track-files', views.ManageTrackFileViewSet, 'track-files')
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^library/',
|
||||||
|
include((library_router.urls, 'instance'), namespace='library')),
|
||||||
|
]
|
|
@ -0,0 +1,49 @@
|
||||||
|
from rest_framework import mixins
|
||||||
|
from rest_framework import response
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import list_route
|
||||||
|
|
||||||
|
from funkwhale_api.music import models as music_models
|
||||||
|
from funkwhale_api.users.permissions import HasUserPermission
|
||||||
|
|
||||||
|
from . import filters
|
||||||
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class ManageTrackFileViewSet(
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
viewsets.GenericViewSet):
|
||||||
|
queryset = (
|
||||||
|
music_models.TrackFile.objects.all()
|
||||||
|
.select_related(
|
||||||
|
'track__artist',
|
||||||
|
'track__album__artist',
|
||||||
|
'library_track')
|
||||||
|
.order_by('-id')
|
||||||
|
)
|
||||||
|
serializer_class = serializers.ManageTrackFileSerializer
|
||||||
|
filter_class = filters.ManageTrackFileFilterSet
|
||||||
|
permission_classes = (HasUserPermission,)
|
||||||
|
required_permissions = ['library']
|
||||||
|
ordering_fields = [
|
||||||
|
'accessed_date',
|
||||||
|
'modification_date',
|
||||||
|
'creation_date',
|
||||||
|
'track__artist__name',
|
||||||
|
'bitrate',
|
||||||
|
'size',
|
||||||
|
'duration',
|
||||||
|
]
|
||||||
|
|
||||||
|
@list_route(methods=['post'])
|
||||||
|
def action(self, request, *args, **kwargs):
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
serializer = serializers.ManageTrackFileActionSerializer(
|
||||||
|
request.data,
|
||||||
|
queryset=queryset,
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
result = serializer.save()
|
||||||
|
return response.Response(result, status=200)
|
|
@ -117,6 +117,11 @@ class ImportJobFactory(factory.django.DjangoModelFactory):
|
||||||
status='finished',
|
status='finished',
|
||||||
audio_file=None,
|
audio_file=None,
|
||||||
)
|
)
|
||||||
|
with_audio_file = factory.Trait(
|
||||||
|
status='finished',
|
||||||
|
audio_file=factory.django.FileField(
|
||||||
|
from_path=os.path.join(SAMPLES_PATH, 'test.ogg')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@registry.register(name='music.FileImportJob')
|
@registry.register(name='music.FileImportJob')
|
||||||
|
|
|
@ -27,7 +27,7 @@ PERMISSIONS_CONFIGURATION = {
|
||||||
},
|
},
|
||||||
'library': {
|
'library': {
|
||||||
'label': 'Manage library',
|
'label': 'Manage library',
|
||||||
'help_text': 'Manage library',
|
'help_text': 'Manage library, delete files, tracks, artists, albums...',
|
||||||
},
|
},
|
||||||
'settings': {
|
'settings': {
|
||||||
'label': 'Manage instance-level settings',
|
'label': 'Manage instance-level settings',
|
||||||
|
|
|
@ -65,3 +65,4 @@ cryptography>=2,<3
|
||||||
# requests-http-signature==0.0.3
|
# requests-http-signature==0.0.3
|
||||||
# clone until the branch is merged and released upstream
|
# clone until the branch is merged and released upstream
|
||||||
git+https://github.com/EliotBerriot/requests-http-signature.git@signature-header-support
|
git+https://github.com/EliotBerriot/requests-http-signature.git@signature-header-support
|
||||||
|
django-cleanup==2.1.0
|
||||||
|
|
|
@ -18,6 +18,17 @@ class TestSerializer(serializers.ActionSerializer):
|
||||||
return {'hello': 'world'}
|
return {'hello': 'world'}
|
||||||
|
|
||||||
|
|
||||||
|
class TestDangerousSerializer(serializers.ActionSerializer):
|
||||||
|
actions = ['test', 'test_dangerous']
|
||||||
|
dangerous_actions = ['test_dangerous']
|
||||||
|
|
||||||
|
def handle_test(self, objects):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_test_dangerous(self, objects):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_action_serializer_validates_action():
|
def test_action_serializer_validates_action():
|
||||||
data = {'objects': 'all', 'action': 'nope'}
|
data = {'objects': 'all', 'action': 'nope'}
|
||||||
serializer = TestSerializer(data, queryset=models.User.objects.none())
|
serializer = TestSerializer(data, queryset=models.User.objects.none())
|
||||||
|
@ -98,3 +109,28 @@ def test_action_serializers_validates_at_least_one_object():
|
||||||
|
|
||||||
assert serializer.is_valid() is False
|
assert serializer.is_valid() is False
|
||||||
assert 'non_field_errors' in serializer.errors
|
assert 'non_field_errors' in serializer.errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_dangerous_actions_refuses_all(factories):
|
||||||
|
factories['users.User']()
|
||||||
|
data = {
|
||||||
|
'objects': 'all',
|
||||||
|
'action': 'test_dangerous',
|
||||||
|
}
|
||||||
|
serializer = TestDangerousSerializer(
|
||||||
|
data, queryset=models.User.objects.all())
|
||||||
|
|
||||||
|
assert serializer.is_valid() is False
|
||||||
|
assert 'non_field_errors' in serializer.errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_dangerous_actions_refuses_not_listed(factories):
|
||||||
|
factories['users.User']()
|
||||||
|
data = {
|
||||||
|
'objects': 'all',
|
||||||
|
'action': 'test',
|
||||||
|
}
|
||||||
|
serializer = TestDangerousSerializer(
|
||||||
|
data, queryset=models.User.objects.all())
|
||||||
|
|
||||||
|
assert serializer.is_valid() is True
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
from funkwhale_api.manage import serializers
|
||||||
|
|
||||||
|
|
||||||
|
def test_manage_track_file_action_delete(factories):
|
||||||
|
tfs = factories['music.TrackFile'](size=5)
|
||||||
|
s = serializers.ManageTrackFileActionSerializer(queryset=None)
|
||||||
|
|
||||||
|
s.handle_delete(tfs.__class__.objects.all())
|
||||||
|
|
||||||
|
assert tfs.__class__.objects.count() == 0
|
|
@ -0,0 +1,26 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from funkwhale_api.manage import serializers
|
||||||
|
from funkwhale_api.manage import views
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('view,permissions,operator', [
|
||||||
|
(views.ManageTrackFileViewSet, ['library'], 'and'),
|
||||||
|
])
|
||||||
|
def test_permissions(assert_user_permission, view, permissions, operator):
|
||||||
|
assert_user_permission(view, permissions, operator)
|
||||||
|
|
||||||
|
|
||||||
|
def test_track_file_view(factories, superuser_api_client):
|
||||||
|
tfs = factories['music.TrackFile'].create_batch(size=5)
|
||||||
|
qs = tfs[0].__class__.objects.order_by('-creation_date')
|
||||||
|
url = reverse('api:v1:manage:library:track-files-list')
|
||||||
|
|
||||||
|
response = superuser_api_client.get(url, {'sort': '-creation_date'})
|
||||||
|
expected = serializers.ManageTrackFileSerializer(
|
||||||
|
qs, many=True, context={'request': response.wsgi_request}).data
|
||||||
|
|
||||||
|
assert response.data['count'] == len(tfs)
|
||||||
|
assert response.data['results'] == expected
|
|
@ -0,0 +1,10 @@
|
||||||
|
Files management interface for users with "library" permission (#223)
|
||||||
|
|
||||||
|
Files management interface
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
This is the first bit of an ongoing work that will span several releases, to
|
||||||
|
bring more powerful library management features to Funkwhale. This iteration
|
||||||
|
includes a basic file management interface where users with the "library"
|
||||||
|
permission can list and search available files, order them using
|
||||||
|
various criterias (size, bitrate, duration...) and delete them.
|
|
@ -0,0 +1 @@
|
||||||
|
Autoremove media files on model instance deletion (#241)
|
|
@ -68,6 +68,12 @@
|
||||||
:title="$t('Pending import requests')">
|
:title="$t('Pending import requests')">
|
||||||
{{ notifications.importRequests }}</div>
|
{{ notifications.importRequests }}</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="item"
|
||||||
|
v-if="$store.state.auth.availablePermissions['library']"
|
||||||
|
:to="{name: 'manage.library.files'}">
|
||||||
|
<i class="book icon"></i>{{ $t('Library') }}
|
||||||
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
class="item"
|
class="item"
|
||||||
v-else-if="$store.state.auth.availablePermissions['upload']"
|
v-else-if="$store.state.auth.availablePermissions['upload']"
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']">
|
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']">
|
||||||
{{ $t('Go') }}</div>
|
{{ $t('Go') }}</div>
|
||||||
<dangerous-button
|
<dangerous-button
|
||||||
v-else :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"
|
v-else-if="!currentAction.isDangerous" :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"
|
||||||
confirm-color="green"
|
confirm-color="green"
|
||||||
color=""
|
color=""
|
||||||
@confirm="launchAction">
|
@confirm="launchAction">
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
<div class="count field">
|
<div class="count field">
|
||||||
<span v-if="selectAll">{{ $t('{% count %} on {% total %} selected', {count: objectsData.count, total: objectsData.count}) }}</span>
|
<span v-if="selectAll">{{ $t('{% count %} on {% total %} selected', {count: objectsData.count, total: objectsData.count}) }}</span>
|
||||||
<span v-else>{{ $t('{% count %} on {% total %} selected', {count: checked.length, total: objectsData.count}) }}</span>
|
<span v-else>{{ $t('{% count %} on {% total %} selected', {count: checked.length, total: objectsData.count}) }}</span>
|
||||||
<template v-if="checkable.length === checked.length">
|
<template v-if="!currentAction.isDangerous && checkable.length === checked.length">
|
||||||
<a @click="selectAll = true" v-if="!selectAll">
|
<a @click="selectAll = true" v-if="!selectAll">
|
||||||
{{ $t('Select all {% total %} elements', {total: objectsData.count}) }}
|
{{ $t('Select all {% total %} elements', {total: objectsData.count}) }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="ui inline form">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="ui field">
|
||||||
|
<label>{{ $t('Search') }}</label>
|
||||||
|
<input type="text" v-model="search" placeholder="Search by title, artist, domain..." />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<i18next tag="label" path="Ordering"/>
|
||||||
|
<select class="ui dropdown" v-model="ordering">
|
||||||
|
<option v-for="option in orderingOptions" :value="option[0]">
|
||||||
|
{{ option[1] }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<i18next tag="label" path="Ordering direction"/>
|
||||||
|
<select class="ui dropdown" v-model="orderingDirection">
|
||||||
|
<option value="+">Ascending</option>
|
||||||
|
<option value="-">Descending</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dimmable">
|
||||||
|
<div v-if="isLoading" class="ui active inverted dimmer">
|
||||||
|
<div class="ui loader"></div>
|
||||||
|
</div>
|
||||||
|
<action-table
|
||||||
|
v-if="result"
|
||||||
|
@action-launched="fetchData"
|
||||||
|
:objects-data="result"
|
||||||
|
:actions="actions"
|
||||||
|
:action-url="'manage/library/track-files/action/'"
|
||||||
|
:filters="actionFilters">
|
||||||
|
<template slot="header-cells">
|
||||||
|
<th>{{ $t('Title') }}</th>
|
||||||
|
<th>{{ $t('Artist') }}</th>
|
||||||
|
<th>{{ $t('Album') }}</th>
|
||||||
|
<th>{{ $t('Import date') }}</th>
|
||||||
|
<th>{{ $t('Type') }}</th>
|
||||||
|
<th>{{ $t('Bitrate') }}</th>
|
||||||
|
<th>{{ $t('Duration') }}</th>
|
||||||
|
<th>{{ $t('Size') }}</th>
|
||||||
|
</template>
|
||||||
|
<template slot="row-cells" slot-scope="scope">
|
||||||
|
<td>
|
||||||
|
<span :title="scope.obj.track.title">{{ scope.obj.track.title|truncate(30) }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span :title="scope.obj.track.artist.name">{{ scope.obj.track.artist.name|truncate(30) }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span :title="scope.obj.track.album.title">{{ scope.obj.track.album.title|truncate(20) }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="scope.obj.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
<td v-if="scope.obj.audio_mimetype">
|
||||||
|
{{ scope.obj.audio_mimetype }}
|
||||||
|
</td>
|
||||||
|
<td v-else>
|
||||||
|
{{ $t('N/A') }}
|
||||||
|
</td>
|
||||||
|
<td v-if="scope.obj.bitrate">
|
||||||
|
{{ scope.obj.bitrate | humanSize }}/s
|
||||||
|
</td>
|
||||||
|
<td v-else>
|
||||||
|
{{ $t('N/A') }}
|
||||||
|
</td>
|
||||||
|
<td v-if="scope.obj.duration">
|
||||||
|
{{ time.parse(scope.obj.duration) }}
|
||||||
|
</td>
|
||||||
|
<td v-else>
|
||||||
|
{{ $t('N/A') }}
|
||||||
|
</td>
|
||||||
|
<td v-if="scope.obj.size">
|
||||||
|
{{ scope.obj.size | humanSize }}
|
||||||
|
</td>
|
||||||
|
<td v-else>
|
||||||
|
{{ $t('N/A') }}
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</action-table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<pagination
|
||||||
|
v-if="result && result.results.length > 0"
|
||||||
|
@page-changed="selectPage"
|
||||||
|
:compact="true"
|
||||||
|
:current="page"
|
||||||
|
:paginate-by="paginateBy"
|
||||||
|
:total="result.count"
|
||||||
|
></pagination>
|
||||||
|
|
||||||
|
<span v-if="result && result.results.length > 0">
|
||||||
|
{{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import time from '@/utils/time'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
import ActionTable from '@/components/common/ActionTable'
|
||||||
|
import OrderingMixin from '@/components/mixins/Ordering'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [OrderingMixin],
|
||||||
|
props: {
|
||||||
|
filters: {type: Object, required: false}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Pagination,
|
||||||
|
ActionTable
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
|
||||||
|
return {
|
||||||
|
time,
|
||||||
|
isLoading: false,
|
||||||
|
result: null,
|
||||||
|
page: 1,
|
||||||
|
paginateBy: 25,
|
||||||
|
search: '',
|
||||||
|
orderingDirection: defaultOrdering.direction || '+',
|
||||||
|
ordering: defaultOrdering.field,
|
||||||
|
orderingOptions: [
|
||||||
|
['creation_date', 'Creation date'],
|
||||||
|
['accessed_date', 'Accessed date'],
|
||||||
|
['modification_date', 'Modification date'],
|
||||||
|
['size', 'Size'],
|
||||||
|
['bitrate', 'Bitrate'],
|
||||||
|
['duration', 'Duration']
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
let params = _.merge({
|
||||||
|
'page': this.page,
|
||||||
|
'page_size': this.paginateBy,
|
||||||
|
'q': this.search,
|
||||||
|
'ordering': this.getOrderingAsString()
|
||||||
|
}, this.filters)
|
||||||
|
let self = this
|
||||||
|
self.isLoading = true
|
||||||
|
self.checked = []
|
||||||
|
axios.get('/manage/library/track-files/', {params: params}).then((response) => {
|
||||||
|
self.result = response.data
|
||||||
|
self.isLoading = false
|
||||||
|
}, error => {
|
||||||
|
self.isLoading = false
|
||||||
|
self.errors = error.backendErrors
|
||||||
|
})
|
||||||
|
},
|
||||||
|
selectPage: function (page) {
|
||||||
|
this.page = page
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
actionFilters () {
|
||||||
|
var currentFilters = {
|
||||||
|
q: this.search
|
||||||
|
}
|
||||||
|
if (this.filters) {
|
||||||
|
return _.merge(currentFilters, this.filters)
|
||||||
|
} else {
|
||||||
|
return currentFilters
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
label: this.$t('Delete'),
|
||||||
|
isDangerous: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search (newValue) {
|
||||||
|
this.page = 1
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
page () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
ordering () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
orderingDirection () {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -29,6 +29,8 @@ import PlaylistDetail from '@/views/playlists/Detail'
|
||||||
import PlaylistList from '@/views/playlists/List'
|
import PlaylistList from '@/views/playlists/List'
|
||||||
import Favorites from '@/components/favorites/List'
|
import Favorites from '@/components/favorites/List'
|
||||||
import AdminSettings from '@/views/admin/Settings'
|
import AdminSettings from '@/views/admin/Settings'
|
||||||
|
import AdminLibraryBase from '@/views/admin/library/Base'
|
||||||
|
import AdminLibraryFilesList from '@/views/admin/library/FilesList'
|
||||||
import FederationBase from '@/views/federation/Base'
|
import FederationBase from '@/views/federation/Base'
|
||||||
import FederationScan from '@/views/federation/Scan'
|
import FederationScan from '@/views/federation/Scan'
|
||||||
import FederationLibraryDetail from '@/views/federation/LibraryDetail'
|
import FederationLibraryDetail from '@/views/federation/LibraryDetail'
|
||||||
|
@ -167,6 +169,17 @@ export default new Router({
|
||||||
{ path: 'libraries/:id', name: 'federation.libraries.detail', component: FederationLibraryDetail, props: true }
|
{ path: 'libraries/:id', name: 'federation.libraries.detail', component: FederationLibraryDetail, props: true }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/manage/library',
|
||||||
|
component: AdminLibraryBase,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'files',
|
||||||
|
name: 'manage.library.files',
|
||||||
|
component: AdminLibraryFilesList
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/library',
|
path: '/library',
|
||||||
component: Library,
|
component: Library,
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<div class="main pusher" v-title="'Manage library'">
|
||||||
|
<div class="ui secondary pointing menu">
|
||||||
|
<router-link
|
||||||
|
class="ui item"
|
||||||
|
:to="{name: 'manage.library.files'}">{{ $t('Files') }}</router-link>
|
||||||
|
</div>
|
||||||
|
<router-view :key="$route.fullPath"></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../../style/vendor/media';
|
||||||
|
|
||||||
|
.main.pusher > .ui.secondary.menu {
|
||||||
|
@include media(">tablet") {
|
||||||
|
margin: 0 2.5rem;
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
padding-top: 1.5em;
|
||||||
|
padding-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div v-title="'Files'">
|
||||||
|
<div class="ui vertical stripe segment">
|
||||||
|
<h2 class="ui header">{{ $t('Library files') }}</h2>
|
||||||
|
<div class="ui hidden divider"></div>
|
||||||
|
<library-files-table :show-library="true"></library-files-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LibraryFilesTable from '@/components/manage/library/FilesTable'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LibraryFilesTable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
Loading…
Reference in New Issue