Merge branch 'master' into develop
This commit is contained in:
		
						commit
						b95710bb3a
					
				| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import html
 | 
					import html
 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
 | 
					import xml.sax.saxutils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django import http
 | 
					from django import http
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
| 
						 | 
					@ -51,7 +52,13 @@ def serve_spa(request):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # let's inject our meta tags in the HTML
 | 
					    # let's inject our meta tags in the HTML
 | 
				
			||||||
    head += "\n" + "\n".join(render_tags(final_tags)) + "\n</head>"
 | 
					    head += "\n" + "\n".join(render_tags(final_tags)) + "\n</head>"
 | 
				
			||||||
 | 
					    css = get_custom_css() or ""
 | 
				
			||||||
 | 
					    if css:
 | 
				
			||||||
 | 
					        # We add the style add the end of the body to ensure it has the highest
 | 
				
			||||||
 | 
					        # priority (since it will come after other stylesheets)
 | 
				
			||||||
 | 
					        body, tail = tail.split("</body>", 1)
 | 
				
			||||||
 | 
					        css = "<style>{}</style>".format(css)
 | 
				
			||||||
 | 
					        tail = body + "\n" + css + "\n</body>" + tail
 | 
				
			||||||
    return http.HttpResponse(head + tail)
 | 
					    return http.HttpResponse(head + tail)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -128,6 +135,14 @@ def get_request_head_tags(request):
 | 
				
			||||||
    return match.func(request, *match.args, **match.kwargs)
 | 
					    return match.func(request, *match.args, **match.kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_custom_css():
 | 
				
			||||||
 | 
					    css = preferences.get("ui__custom_css").strip()
 | 
				
			||||||
 | 
					    if not css:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return xml.sax.saxutils.escape(css)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SPAFallbackMiddleware:
 | 
					class SPAFallbackMiddleware:
 | 
				
			||||||
    def __init__(self, get_response):
 | 
					    def __init__(self, get_response):
 | 
				
			||||||
        self.get_response = get_response
 | 
					        self.get_response = get_response
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ from dynamic_preferences.registries import global_preferences_registry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
raven = types.Section("raven")
 | 
					raven = types.Section("raven")
 | 
				
			||||||
instance = types.Section("instance")
 | 
					instance = types.Section("instance")
 | 
				
			||||||
 | 
					ui = types.Section("ui")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@global_preferences_registry.register
 | 
					@global_preferences_registry.register
 | 
				
			||||||
| 
						 | 
					@ -98,3 +99,19 @@ class InstanceNodeinfoStatsEnabled(types.BooleanPreference):
 | 
				
			||||||
        "Disable this if you don't want to share usage and library statistics "
 | 
					        "Disable this if you don't want to share usage and library statistics "
 | 
				
			||||||
        "in the nodeinfo endpoint but don't want to disable it completely."
 | 
					        "in the nodeinfo endpoint but don't want to disable it completely."
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@global_preferences_registry.register
 | 
				
			||||||
 | 
					class CustomCSS(types.StringPreference):
 | 
				
			||||||
 | 
					    show_in_api = True
 | 
				
			||||||
 | 
					    section = ui
 | 
				
			||||||
 | 
					    name = "custom_css"
 | 
				
			||||||
 | 
					    verbose_name = "Custom CSS code"
 | 
				
			||||||
 | 
					    default = ""
 | 
				
			||||||
 | 
					    help_text = (
 | 
				
			||||||
 | 
					        "Custom CSS code, to be included in a <style> tag on all pages. "
 | 
				
			||||||
 | 
					        "Loading third-party resources such as fonts or images can affect the performance "
 | 
				
			||||||
 | 
					        "of the app and the privacy of your users."
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    widget = widgets.Textarea
 | 
				
			||||||
 | 
					    field_kwargs = {"required": False}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -256,7 +256,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
 | 
				
			||||||
        if max_bitrate:
 | 
					        if max_bitrate:
 | 
				
			||||||
            max_bitrate = max_bitrate * 1000
 | 
					            max_bitrate = max_bitrate * 1000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        format = data.get("format", "raw") or None
 | 
					        format = data.get("format") or None
 | 
				
			||||||
        if max_bitrate and not format:
 | 
					        if max_bitrate and not format:
 | 
				
			||||||
            # specific bitrate requested, but no format specified
 | 
					            # specific bitrate requested, but no format specified
 | 
				
			||||||
            # so we use a default one, cf #867. This helps with clients
 | 
					            # so we use a default one, cf #867. This helps with clients
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -141,3 +141,47 @@ def test_get_route_head_tags(mocker, settings):
 | 
				
			||||||
    assert tags == match.func.return_value
 | 
					    assert tags == match.func.return_value
 | 
				
			||||||
    match.func.assert_called_once_with(request, *[], **{"pk": 42})
 | 
					    match.func.assert_called_once_with(request, *[], **{"pk": 42})
 | 
				
			||||||
    resolve.assert_called_once_with(request.path, urlconf=settings.SPA_URLCONF)
 | 
					    resolve.assert_called_once_with(request.path, urlconf=settings.SPA_URLCONF)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_serve_spa_includes_custom_css(mocker, no_api_auth):
 | 
				
			||||||
 | 
					    request = mocker.Mock(path="/")
 | 
				
			||||||
 | 
					    mocker.patch.object(
 | 
				
			||||||
 | 
					        middleware,
 | 
				
			||||||
 | 
					        "get_spa_html",
 | 
				
			||||||
 | 
					        return_value="<html><head></head><body></body></html>",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    mocker.patch.object(middleware, "get_default_head_tags", return_value=[])
 | 
				
			||||||
 | 
					    mocker.patch.object(middleware, "get_request_head_tags", return_value=[])
 | 
				
			||||||
 | 
					    get_custom_css = mocker.patch.object(
 | 
				
			||||||
 | 
					        middleware, "get_custom_css", return_value="body { background: black; }"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    response = middleware.serve_spa(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert response.status_code == 200
 | 
				
			||||||
 | 
					    expected = [
 | 
				
			||||||
 | 
					        "<html><head>\n\n</head><body>",
 | 
				
			||||||
 | 
					        "<style>body { background: black; }</style>",
 | 
				
			||||||
 | 
					        "</body></html>",
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    get_custom_css.assert_called_once_with()
 | 
				
			||||||
 | 
					    assert response.content == "\n".join(expected).encode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "custom_css, expected",
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					        ("body { background: black; }", "body { background: black; }"),
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            "body { injection: </style> & Hello",
 | 
				
			||||||
 | 
					            "body { injection: </style> & Hello",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            'body { background: url("image/url"); }',
 | 
				
			||||||
 | 
					            'body { background: url("image/url"); }',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_get_custom_css(preferences, custom_css, expected):
 | 
				
			||||||
 | 
					    preferences["ui__custom_css"] = custom_css
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert middleware.get_custom_css() == expected
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,8 +29,8 @@ from rest_framework.test import APIClient, APIRequestFactory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from funkwhale_api.activity import record
 | 
					from funkwhale_api.activity import record
 | 
				
			||||||
from funkwhale_api.federation import actors
 | 
					from funkwhale_api.federation import actors
 | 
				
			||||||
from funkwhale_api.music import licenses
 | 
					 | 
				
			||||||
from funkwhale_api.moderation import mrf
 | 
					from funkwhale_api.moderation import mrf
 | 
				
			||||||
 | 
					from funkwhale_api.music import licenses
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pytest_plugins = "aiohttp.pytest_plugin"
 | 
					pytest_plugins = "aiohttp.pytest_plugin"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -288,15 +288,16 @@ def test_stream_transcode(
 | 
				
			||||||
    mocker,
 | 
					    mocker,
 | 
				
			||||||
    settings,
 | 
					    settings,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
 | 
					    upload = factories["music.Upload"](playable=True)
 | 
				
			||||||
 | 
					    params = {"id": upload.track.pk, "maxBitRate": max_bitrate}
 | 
				
			||||||
 | 
					    if format:
 | 
				
			||||||
 | 
					        params["format"] = format
 | 
				
			||||||
    settings.SUBSONIC_DEFAULT_TRANSCODING_FORMAT = default_transcoding_format
 | 
					    settings.SUBSONIC_DEFAULT_TRANSCODING_FORMAT = default_transcoding_format
 | 
				
			||||||
    url = reverse("api:subsonic-stream")
 | 
					    url = reverse("api:subsonic-stream")
 | 
				
			||||||
    mocked_serve = mocker.patch.object(
 | 
					    mocked_serve = mocker.patch.object(
 | 
				
			||||||
        music_views, "handle_serve", return_value=Response()
 | 
					        music_views, "handle_serve", return_value=Response()
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    upload = factories["music.Upload"](playable=True)
 | 
					    response = logged_in_api_client.get(url, params)
 | 
				
			||||||
    response = logged_in_api_client.get(
 | 
					 | 
				
			||||||
        url, {"id": upload.track.pk, "maxBitRate": max_bitrate, "format": format}
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mocked_serve.assert_called_once_with(
 | 
					    mocked_serve.assert_called_once_with(
 | 
				
			||||||
        upload=upload,
 | 
					        upload=upload,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					Fixed remaining transcoding issue with Subsonic API (#867)
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					Admins can now add custom CSS from their pod settings (#879)
 | 
				
			||||||
| 
						 | 
					@ -364,7 +364,11 @@ export default {
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      let image = this.$refs.cover
 | 
					      let image = this.$refs.cover
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
        this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
 | 
					        this.ambiantColors = ColorThief.prototype.getPalette(image, 4).slice(0, 4)
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        console.log('Cannot generate player background from cover image, likely a cross-origin tainted canvas issue')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    handleError({ sound, error }) {
 | 
					    handleError({ sound, error }) {
 | 
				
			||||||
      this.$store.commit("player/isLoadingAudio", false)
 | 
					      this.$store.commit("player/isLoadingAudio", false)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@
 | 
				
			||||||
            <tbody>
 | 
					            <tbody>
 | 
				
			||||||
              <tr v-for="track in tracks">
 | 
					              <tr v-for="track in tracks">
 | 
				
			||||||
                <td class="play-cell">
 | 
					                <td class="play-cell">
 | 
				
			||||||
                  <play-button :class="['basic', {orange: isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :track="track"></play-button>
 | 
					                  <play-button :class="['basic', {orange: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :track="track"></play-button>
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
                <td class="content-cell" colspan="5">
 | 
					                <td class="content-cell" colspan="5">
 | 
				
			||||||
                  <track-favorite-icon :track="track"></track-favorite-icon>
 | 
					                  <track-favorite-icon :track="track"></track-favorite-icon>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <tr>
 | 
					  <tr>
 | 
				
			||||||
    <td>
 | 
					    <td>
 | 
				
			||||||
      <play-button :class="['basic', {orange: isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :is-playable="playable" :track="track"></play-button>
 | 
					      <play-button :class="['basic', {orange: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :is-playable="playable" :track="track"></play-button>
 | 
				
			||||||
    </td>
 | 
					    </td>
 | 
				
			||||||
    <td>
 | 
					    <td>
 | 
				
			||||||
      <img class="ui mini image" v-if="track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.small_square_crop)">
 | 
					      <img class="ui mini image" v-if="track.album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](track.album.cover.small_square_crop)">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -85,6 +85,7 @@ export default {
 | 
				
			||||||
      let moderationLabel = this.$pgettext('Content/Admin/Menu', 'Moderation')
 | 
					      let moderationLabel = this.$pgettext('Content/Admin/Menu', 'Moderation')
 | 
				
			||||||
      let subsonicLabel = this.$pgettext('Content/Admin/Menu', 'Subsonic')
 | 
					      let subsonicLabel = this.$pgettext('Content/Admin/Menu', 'Subsonic')
 | 
				
			||||||
      let statisticsLabel = this.$pgettext('Content/Admin/Menu', 'Statistics')
 | 
					      let statisticsLabel = this.$pgettext('Content/Admin/Menu', 'Statistics')
 | 
				
			||||||
 | 
					      let uiLabel = this.$pgettext('Content/Admin/Menu', 'User Interface')
 | 
				
			||||||
      let errorLabel = this.$pgettext('Content/Admin/Menu', 'Error reporting')
 | 
					      let errorLabel = this.$pgettext('Content/Admin/Menu', 'Error reporting')
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
| 
						 | 
					@ -143,6 +144,11 @@ export default {
 | 
				
			||||||
          id: "subsonic",
 | 
					          id: "subsonic",
 | 
				
			||||||
          settings: ["subsonic__enabled"]
 | 
					          settings: ["subsonic__enabled"]
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          label: uiLabel,
 | 
				
			||||||
 | 
					          id: "ui",
 | 
				
			||||||
 | 
					          settings: ["ui__custom_css"]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          label: statisticsLabel,
 | 
					          label: statisticsLabel,
 | 
				
			||||||
          id: "statistics",
 | 
					          id: "statistics",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue