diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index 5031e69ba8030f3db1ec4bffa22bd01af1151c5f..571bc4ddd42509320a9ec4ec8a9f3e503d9708da 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -12,7 +12,7 @@ def guess_mimetype(f): t = magic.from_buffer(f.read(b), mime=True) if not t.startswith("audio/"): # failure, we try guessing by extension - mt, _ = mimetypes.guess_type(f.path) + mt, _ = mimetypes.guess_type(f.name) if mt: t = mt return t diff --git a/api/tests/music/test_utils.py b/api/tests/music/test_utils.py index 87eaddc435d9270e29a0750a60f32b2af7f6e74f..982422c34a07978d9438d44044cd59f550682f30 100644 --- a/api/tests/music/test_utils.py +++ b/api/tests/music/test_utils.py @@ -36,3 +36,12 @@ def test_get_audio_file_data(name, expected): result = utils.get_audio_file_data(f) 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" diff --git a/changes/changelog.d/814.enhancement b/changes/changelog.d/814.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..c93b1983c954bbcec69ee5f3b0f6912d00ed7b5f --- /dev/null +++ b/changes/changelog.d/814.enhancement @@ -0,0 +1 @@ +Added copy-to-clipboard button with Subsonic password input (#814) diff --git a/changes/changelog.d/833.bugfix b/changes/changelog.d/833.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..06ce086f56832952ddf685e46ade30ddecfbb936 --- /dev/null +++ b/changes/changelog.d/833.bugfix @@ -0,0 +1 @@ +Fixed broken translation on home and track detail page (#833) diff --git a/changes/changelog.d/855.bugfix b/changes/changelog.d/855.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..171865ed7737266f59f5d9757b5457a0581e8276 --- /dev/null +++ b/changes/changelog.d/855.bugfix @@ -0,0 +1 @@ +Fixed secondary menus truncated on narrow screens (#855) diff --git a/changes/changelog.d/857.bugfix b/changes/changelog.d/857.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..5d525e3d9fd16f0cd0fff579be8edefb181c4b66 --- /dev/null +++ b/changes/changelog.d/857.bugfix @@ -0,0 +1 @@ +Fix broken upload for specific files when using S3 storage (#857) diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue index b88369d952a816c0a4b9acf7db0555b8290049e3..9393d9b7850cf4e40594bbd4fdc47774753a0f1e 100644 --- a/front/src/components/Home.vue +++ b/front/src/components/Home.vue @@ -68,12 +68,7 @@ <div class="ui list"> <div class="item"> <i class="tag icon"></i> - <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 class="content" v-html="musicbrainzItem"></div> </div> <div class="item"> <i class="plus icon"></i> @@ -147,6 +142,10 @@ export default { return { 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}) } } } diff --git a/front/src/components/auth/SubsonicTokenForm.vue b/front/src/components/auth/SubsonicTokenForm.vue index 0184074e294365a15e453db7184644d66d75796a..fdd9f5e107b2de27b28cb5099b56751b9be180a6 100644 --- a/front/src/components/auth/SubsonicTokenForm.vue +++ b/front/src/components/auth/SubsonicTokenForm.vue @@ -24,7 +24,12 @@ </div> <template v-if="subsonicEnabled"> <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> <dangerous-button v-if="token" @@ -69,7 +74,8 @@ export default { errors: [], success: false, isLoading: false, - successMessage: '' + successMessage: '', + showToken: false } }, created () { @@ -98,6 +104,7 @@ export default { let self = this let url = `users/users/${this.$store.state.auth.username}/subsonic-token/` return axios.post(url, {}).then(response => { + self.showToken = true self.token = response.data['subsonic_api_token'] self.isLoading = false self.success = true diff --git a/front/src/components/forms/PasswordInput.vue b/front/src/components/forms/PasswordInput.vue index d57e3017f3010000e6e45932e25e2092982ef246..702be4f66a5642493012afd70ae3ea7d949dd134 100644 --- a/front/src/components/forms/PasswordInput.vue +++ b/front/src/components/forms/PasswordInput.vue @@ -10,20 +10,37 @@ <span @click="showPassword = !showPassword" :title="labels.title" class="ui icon button"> <i class="eye icon"></i> </span> + <button v-if="copyButton" @click.prevent="copy" class="ui icon button" :title="labels.copy"> + <i class="copy icon"></i> + </button> </div> </template> <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 { - props: ['value', 'index'], + props: ['value', 'index', 'defaultShow', 'copyButton'], data () { return { - showPassword: false + showPassword: this.defaultShow || false, } }, computed: { labels () { 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 () { @@ -32,6 +49,11 @@ export default { } return 'password' } + }, + methods: { + copy () { + copyStringToClipboard(this.value) + } } } </script> diff --git a/front/src/components/library/TrackBase.vue b/front/src/components/library/TrackBase.vue index a968f8b0cf626a37b596832184c6e9782f65c95c..4edd00c5de9ae92f32254636bec9bc19f4643751 100644 --- a/front/src/components/library/TrackBase.vue +++ b/front/src/components/library/TrackBase.vue @@ -14,11 +14,7 @@ <i class="circular inverted music orange icon"></i> <div class="content"> {{ track.title }} - <div class="sub header"> - <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 class="sub header" v-html="subtitle"></div> </div> </h2> <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: { id() { diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss index bc527415c7658b82a418f0f678398f01f8627d3e..4c6c6d61e4893dbf2e400426afaa717af648ab54 100644 --- a/front/src/style/_main.scss +++ b/front/src/style/_main.scss @@ -131,6 +131,7 @@ body { margin-left: 0; margin-right: 0; border: none; + overflow-y: auto; .ui.item { border: none; border-bottom-style: none;