Merge branch 'master' into develop
This commit is contained in:
commit
371dc01205
|
@ -12,7 +12,7 @@ def guess_mimetype(f):
|
||||||
t = magic.from_buffer(f.read(b), mime=True)
|
t = magic.from_buffer(f.read(b), mime=True)
|
||||||
if not t.startswith("audio/"):
|
if not t.startswith("audio/"):
|
||||||
# failure, we try guessing by extension
|
# failure, we try guessing by extension
|
||||||
mt, _ = mimetypes.guess_type(f.path)
|
mt, _ = mimetypes.guess_type(f.name)
|
||||||
if mt:
|
if mt:
|
||||||
t = mt
|
t = mt
|
||||||
return t
|
return t
|
||||||
|
|
|
@ -36,3 +36,12 @@ def test_get_audio_file_data(name, expected):
|
||||||
result = utils.get_audio_file_data(f)
|
result = utils.get_audio_file_data(f)
|
||||||
|
|
||||||
assert result == expected
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_guess_mimetype_dont_crash_with_s3(factories, mocker, settings):
|
||||||
|
"""See #857"""
|
||||||
|
settings.DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIS3Boto3Storage"
|
||||||
|
mocker.patch("magic.from_buffer", return_value="none")
|
||||||
|
f = factories["music.Upload"].build(audio_file__filename="test.mp3")
|
||||||
|
|
||||||
|
assert utils.guess_mimetype(f.audio_file) == "audio/mpeg"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Added copy-to-clipboard button with Subsonic password input (#814)
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed broken translation on home and track detail page (#833)
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed secondary menus truncated on narrow screens (#855)
|
|
@ -0,0 +1 @@
|
||||||
|
Fix broken upload for specific files when using S3 storage (#857)
|
|
@ -68,12 +68,7 @@
|
||||||
<div class="ui list">
|
<div class="ui list">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="tag icon"></i>
|
<i class="tag icon"></i>
|
||||||
<div
|
<div class="content" v-html="musicbrainzItem"></div>
|
||||||
class="content"
|
|
||||||
v-translate="{url: musicbrainzUrl}"
|
|
||||||
translate-context="Content/Home/List item/Verb">
|
|
||||||
Get quality metadata about your music thanks to <a href="%{ url }" target="_blank">MusicBrainz</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<i class="plus icon"></i>
|
<i class="plus icon"></i>
|
||||||
|
@ -147,6 +142,10 @@ export default {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('Head/Home/Title', "Welcome")
|
title: this.$pgettext('Head/Home/Title', "Welcome")
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
musicbrainzItem () {
|
||||||
|
let msg = this.$pgettext('Content/Home/List item/Verb', 'Get quality metadata about your music thanks to <a href="%{ url }" target="_blank">MusicBrainz</a>')
|
||||||
|
return this.$gettextInterpolate(msg, {url: this.musicbrainzUrl})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,12 @@
|
||||||
</div>
|
</div>
|
||||||
<template v-if="subsonicEnabled">
|
<template v-if="subsonicEnabled">
|
||||||
<div v-if="token" class="field">
|
<div v-if="token" class="field">
|
||||||
<password-input v-model="token" />
|
<password-input
|
||||||
|
ref="passwordInput"
|
||||||
|
v-model="token"
|
||||||
|
:key="token"
|
||||||
|
:copy-button="true"
|
||||||
|
:default-show="showToken"/>
|
||||||
</div>
|
</div>
|
||||||
<dangerous-button
|
<dangerous-button
|
||||||
v-if="token"
|
v-if="token"
|
||||||
|
@ -69,7 +74,8 @@ export default {
|
||||||
errors: [],
|
errors: [],
|
||||||
success: false,
|
success: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
successMessage: ''
|
successMessage: '',
|
||||||
|
showToken: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -98,6 +104,7 @@ export default {
|
||||||
let self = this
|
let self = this
|
||||||
let url = `users/users/${this.$store.state.auth.username}/subsonic-token/`
|
let url = `users/users/${this.$store.state.auth.username}/subsonic-token/`
|
||||||
return axios.post(url, {}).then(response => {
|
return axios.post(url, {}).then(response => {
|
||||||
|
self.showToken = true
|
||||||
self.token = response.data['subsonic_api_token']
|
self.token = response.data['subsonic_api_token']
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.success = true
|
self.success = true
|
||||||
|
|
|
@ -10,20 +10,37 @@
|
||||||
<span @click="showPassword = !showPassword" :title="labels.title" class="ui icon button">
|
<span @click="showPassword = !showPassword" :title="labels.title" class="ui icon button">
|
||||||
<i class="eye icon"></i>
|
<i class="eye icon"></i>
|
||||||
</span>
|
</span>
|
||||||
|
<button v-if="copyButton" @click.prevent="copy" class="ui icon button" :title="labels.copy">
|
||||||
|
<i class="copy icon"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
function copyStringToClipboard (str) {
|
||||||
|
// cf https://techoverflow.net/2018/03/30/copying-strings-to-the-clipboard-using-pure-javascript/
|
||||||
|
let el = document.createElement('textarea');
|
||||||
|
el.value = str;
|
||||||
|
el.setAttribute('readonly', '');
|
||||||
|
el.style = {position: 'absolute', left: '-9999px'};
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['value', 'index'],
|
props: ['value', 'index', 'defaultShow', 'copyButton'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showPassword: false
|
showPassword: this.defaultShow || false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
title: this.$pgettext('Content/Settings/Button.Tooltip/Verb', 'Show/hide password')
|
title: this.$pgettext('Content/Settings/Button.Tooltip/Verb', 'Show/hide password'),
|
||||||
|
copy: this.$pgettext('*/*/Button.Label/Short, Verb', 'Copy')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
passwordInputType () {
|
passwordInputType () {
|
||||||
|
@ -32,6 +49,11 @@ export default {
|
||||||
}
|
}
|
||||||
return 'password'
|
return 'password'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copy () {
|
||||||
|
copyStringToClipboard(this.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -14,11 +14,7 @@
|
||||||
<i class="circular inverted music orange icon"></i>
|
<i class="circular inverted music orange icon"></i>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ track.title }}
|
{{ track.title }}
|
||||||
<div class="sub header">
|
<div class="sub header" v-html="subtitle"></div>
|
||||||
<div translate-context="Content/Track/Paragraph"
|
|
||||||
v-translate="{album: track.album.title, artist: track.artist.name, albumUrl: albumUrl, artistUrl: artistUrl}"
|
|
||||||
>From album <a class="internal" href="%{ albumUrl }">%{ album }</a> by <a class="internal" href="%{ artistUrl }">%{ artist }</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
|
@ -230,6 +226,10 @@ export default {
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
subtitle () {
|
||||||
|
let msg = this.$pgettext('Content/Track/Paragraph', 'From album <a class="internal" href="%{ albumUrl }">%{ album }</a> by <a class="internal" href="%{ artistUrl }">%{ artist }</a>')
|
||||||
|
return this.$gettextInterpolate(msg, {album: this.track.album.title, artist: this.track.artist.name, albumUrl: this.albumUrl, artistUrl: this.artistUrl})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
id() {
|
id() {
|
||||||
|
|
|
@ -131,6 +131,7 @@ body {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
overflow-y: auto;
|
||||||
.ui.item {
|
.ui.item {
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom-style: none;
|
border-bottom-style: none;
|
||||||
|
|
Loading…
Reference in New Issue