Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
interfect
funkwhale
Commits
3a31248a
Verified
Commit
3a31248a
authored
Apr 12, 2018
by
Eliot Berriot
Browse files
Can now import library tracks from front-end
parent
2cef58e6
Changes
7
Hide whitespace changes
Inline
Side-by-side
api/funkwhale_api/federation/models.py
View file @
3a31248a
...
...
@@ -163,3 +163,10 @@ class LibraryTrack(models.Model):
title
=
models
.
CharField
(
max_length
=
500
)
metadata
=
JSONField
(
default
=
{},
max_length
=
10000
,
encoder
=
DjangoJSONEncoder
)
@
property
def
mbid
(
self
):
try
:
return
self
.
metadata
[
'recording'
][
'musicbrainz_id'
]
except
KeyError
:
pass
api/funkwhale_api/music/forms.py
View file @
3a31248a
...
...
@@ -19,5 +19,5 @@ class TranscodeForm(forms.Form):
choices
=
BITRATE_CHOICES
,
required
=
False
)
track_file
=
forms
.
ModelChoiceField
(
queryset
=
models
.
TrackFile
.
objects
.
all
(
)
queryset
=
models
.
TrackFile
.
objects
.
exclude
(
audio_file__isnull
=
True
)
)
api/funkwhale_api/music/serializers.py
View file @
3a31248a
...
...
@@ -3,8 +3,9 @@ from rest_framework import serializers
from
taggit.models
import
Tag
from
funkwhale_api.activity
import
serializers
as
activity_serializers
from
funkwhale_api.federation.serializers
import
AP_CONTEXT
from
funkwhale_api.federation
import
utils
as
federation_utils
from
funkwhale_api.federation.models
import
LibraryTrack
from
funkwhale_api.federation.serializers
import
AP_CONTEXT
from
.
import
models
...
...
@@ -153,3 +154,25 @@ class TrackActivitySerializer(activity_serializers.ModelSerializer):
def
get_type
(
self
,
obj
):
return
'Audio'
class
SubmitFederationTracksSerializer
(
serializers
.
Serializer
):
library_tracks
=
serializers
.
PrimaryKeyRelatedField
(
many
=
True
,
queryset
=
LibraryTrack
.
objects
.
filter
(
local_track_file__isnull
=
True
),
)
@
transaction
.
atomic
def
save
(
self
,
**
kwargs
):
batch
=
models
.
ImportBatch
.
objects
.
create
(
source
=
'federation'
,
**
kwargs
)
for
lt
in
self
.
validated_data
[
'library_tracks'
]:
models
.
ImportJob
.
objects
.
create
(
batch
=
batch
,
library_track
=
lt
,
mbid
=
lt
.
mbid
,
source
=
lt
.
url
,
)
return
batch
api/funkwhale_api/music/views.py
View file @
3a31248a
import
ffmpeg
import
os
import
json
import
logging
import
subprocess
import
unicodedata
import
urllib
...
...
@@ -40,6 +41,8 @@ from . import serializers
from
.
import
tasks
from
.
import
utils
logger
=
logging
.
getLogger
(
__name__
)
class
SearchMixin
(
object
):
search_fields
=
[]
...
...
@@ -223,6 +226,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
headers
=
{
'Content-Type'
:
'application/activity+json'
})
logger
.
debug
(
'Proxying media request to %s'
,
library_track
.
audio_url
)
response
=
StreamingHttpResponse
(
remote_response
.
iter_content
())
else
:
response
=
Response
()
...
...
@@ -249,6 +254,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
return
Response
(
form
.
errors
,
status
=
400
)
f
=
form
.
cleaned_data
[
'track_file'
]
if
not
f
.
audio_file
:
return
Response
(
status
=
400
)
output_kwargs
=
{
'format'
:
form
.
cleaned_data
[
'to'
]
}
...
...
@@ -392,6 +399,22 @@ class SubmitViewSet(viewsets.ViewSet):
data
,
request
,
batch
=
None
,
import_request
=
import_request
)
return
Response
(
import_data
)
@
list_route
(
methods
=
[
'post'
])
@
transaction
.
non_atomic_requests
def
federation
(
self
,
request
,
*
args
,
**
kwargs
):
serializer
=
serializers
.
SubmitFederationTracksSerializer
(
data
=
request
.
data
)
serializer
.
is_valid
(
raise_exception
=
True
)
batch
=
serializer
.
save
(
submitted_by
=
request
.
user
)
for
job
in
batch
.
jobs
.
all
():
funkwhale_utils
.
on_commit
(
tasks
.
import_job_run
.
delay
,
import_job_id
=
job
.
pk
,
use_acoustid
=
False
,
)
return
Response
({
'id'
:
batch
.
id
},
status
=
201
)
@
transaction
.
atomic
def
_import_album
(
self
,
data
,
request
,
batch
=
None
,
import_request
=
None
):
# we import the whole album here to prevent race conditions that occurs
...
...
api/tests/music/test_views.py
View file @
3a31248a
import
io
import
pytest
from
django.urls
import
reverse
from
funkwhale_api.music
import
views
from
funkwhale_api.federation
import
actors
...
...
@@ -83,3 +85,21 @@ def test_can_proxy_remote_track(
assert
response
.
status_code
==
200
assert
list
(
response
.
streaming_content
)
==
[
b
't'
,
b
'e'
,
b
's'
,
b
't'
]
assert
response
[
'Content-Type'
]
==
track_file
.
library_track
.
audio_mimetype
def
test_can_create_import_from_federation_tracks
(
factories
,
superuser_api_client
,
mocker
):
lts
=
factories
[
'federation.LibraryTrack'
].
create_batch
(
size
=
5
)
mocker
.
patch
(
'funkwhale_api.music.tasks.import_job_run'
)
payload
=
{
'library_tracks'
:
[
l
.
pk
for
l
in
lts
]
}
url
=
reverse
(
'api:v1:submit-federation'
)
response
=
superuser_api_client
.
post
(
url
,
payload
)
assert
response
.
status_code
==
201
batch
=
superuser_api_client
.
user
.
imports
.
latest
(
'id'
)
assert
batch
.
jobs
.
count
()
==
5
for
i
,
job
in
enumerate
(
batch
.
jobs
.
all
()):
assert
job
.
library_track
==
lts
[
i
]
front/src/components/federation/LibraryTrackTable.vue
0 → 100644
View file @
3a31248a
<
template
>
<div>
<div
class=
"ui inline form"
>
<input
type=
"text"
v-model=
"search"
placeholder=
"Search by title, artist, domain..."
/>
</div>
<table
v-if=
"result"
class=
"ui compact very basic single line unstackable table"
>
<thead>
<tr>
<th
colspan=
"1"
>
<div
class=
"ui checkbox"
>
<input
type=
"checkbox"
@
change=
"toggleCheckAll"
:checked=
"result.results.length === checked.length"
><label>
</label>
</div>
</th>
<th>
Title
</th>
<th>
Artist
</th>
<th>
Album
</th>
<th>
Published date
</th>
</tr>
</thead>
<tbody>
<tr
v-for=
"track in result.results"
>
<td
class=
"collapsing"
>
<div
v-if=
"!track.local_track_file"
class=
"ui checkbox"
>
<input
type=
"checkbox"
@
change=
"toggleCheck(track.id)"
:checked=
"checked.indexOf(track.id) > -1"
><label>
</label>
</div>
<div
v-else
class=
"ui label"
>
In library
</div>
</td>
<td>
{{
track
.
title
}}
</td>
<td>
{{
track
.
artist_name
}}
</td>
<td>
{{
track
.
album_title
}}
</td>
<td>
<human-date
:date=
"track.published_date"
></human-date>
</td>
</tr>
</tbody>
<tfoot
class=
"full-width"
>
<tr>
<th
colspan=
"5"
>
<button
@
click=
"launchImport"
:disabled=
"checked.length === 0 || isImporting"
:class=
"['ui', 'green',
{loading: isImporting}, 'button']">Import
{{
checked
.
length
}}
tracks
</button>
</th>
</tr>
</tfoot>
</table>
</div>
</
template
>
<
script
>
import
axios
from
'
axios
'
import
_
from
'
lodash
'
export
default
{
props
:
[
'
filters
'
],
data
()
{
return
{
isLoading
:
false
,
result
:
null
,
page
:
1
,
paginateBy
:
50
,
search
:
''
,
checked
:
{},
isImporting
:
false
}
},
created
()
{
this
.
fetchData
()
},
methods
:
{
fetchData
()
{
let
params
=
_
.
merge
({
'
page
'
:
this
.
page
,
'
paginate_by
'
:
this
.
paginateBy
,
'
q
'
:
this
.
search
},
this
.
filters
)
let
self
=
this
self
.
isLoading
=
true
self
.
checked
=
[]
axios
.
get
(
'
/federation/library-tracks/
'
,
{
params
:
params
}).
then
((
response
)
=>
{
self
.
result
=
response
.
data
self
.
isLoading
=
false
},
error
=>
{
self
.
isLoading
=
false
self
.
errors
=
error
.
backendErrors
})
},
launchImport
()
{
let
self
=
this
self
.
isImporting
=
true
let
payload
=
{
library_tracks
:
this
.
checked
}
axios
.
post
(
'
/submit/federation/
'
,
payload
).
then
((
response
)
=>
{
console
.
log
(
'
Triggered import
'
,
response
.
data
)
self
.
isImporting
=
false
self
.
fetchData
()
},
error
=>
{
self
.
isImporting
=
false
self
.
errors
=
error
.
backendErrors
})
},
toggleCheckAll
()
{
if
(
this
.
checked
.
length
===
this
.
result
.
results
.
length
)
{
// we uncheck
this
.
checked
=
[]
}
else
{
this
.
checked
=
this
.
result
.
results
.
map
(
t
=>
{
return
t
.
id
})
}
},
toggleCheck
(
id
)
{
if
(
this
.
checked
.
indexOf
(
id
)
>
-
1
)
{
// we uncheck
this
.
checked
.
splice
(
this
.
checked
.
indexOf
(
id
),
1
)
}
else
{
this
.
checked
.
push
(
id
)
}
}
},
watch
:
{
search
(
newValue
)
{
if
(
newValue
.
length
>
0
)
{
this
.
fetchData
()
}
}
}
}
</
script
>
front/src/views/federation/LibraryDetail.vue
View file @
3a31248a
...
...
@@ -82,6 +82,16 @@
<td>
<human-date
v-if=
"object.fetched_date"
:date=
"object.fetched_date"
></human-date>
<
template
v-else
>
Never
</
template
>
<button
@
click=
"scan"
v-if=
"!scanTrigerred"
:class=
"['ui', 'basic', {loading: isScanLoading}, 'button']"
>
<i
class=
"sync icon"
></i>
Trigger scan
</button>
<button
v-else
class=
"ui success button"
>
<i
class=
"check icon"
></i>
Scan triggered!
</button>
</td>
<td></td>
</tr>
...
...
@@ -91,6 +101,7 @@
</div>
<div
class=
"ui vertical stripe segment"
>
<h2>
Tracks available in this library
</h2>
<library-track-table
:filters=
"{library: id}"
></library-track-table>
<div
class=
"ui stackable doubling three column grid"
>
</div>
</div>
...
...
@@ -102,13 +113,19 @@
import
axios
from
'
axios
'
import
logger
from
'
@/logging
'
import
LibraryTrackTable
from
'
@/components/federation/LibraryTrackTable
'
export
default
{
props
:
[
'
id
'
],
components
:
{},
components
:
{
LibraryTrackTable
},
data
()
{
return
{
isLoading
:
true
,
object
:
null
isScanLoading
:
false
,
object
:
null
,
scanTrigerred
:
false
}
},
created
()
{
...
...
@@ -125,6 +142,18 @@ export default {
self
.
isLoading
=
false
})
},
scan
(
until
)
{
var
self
=
this
this
.
isScanLoading
=
true
let
data
=
{}
let
url
=
'
federation/libraries/
'
+
this
.
id
+
'
/scan/
'
logger
.
default
.
debug
(
'
Triggering scan for library "
'
+
this
.
id
+
'
"
'
)
axios
.
post
(
url
,
data
).
then
((
response
)
=>
{
self
.
scanTrigerred
=
true
logger
.
default
.
debug
(
'
Scan triggered with id
'
,
response
.
data
)
self
.
isScanLoading
=
false
})
},
update
(
attr
)
{
let
newValue
=
this
.
object
[
attr
]
let
params
=
{}
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment