~phw/discourse-listenbrainz

13829d0c88d4ac26afd89495443b7028c0cb8711 — Philipp Wolfer 9 months ago 80f93b1
Make showing the pinned recording an option of the listen card

Simplifies both the code and the user configuration, with a clear way
to configure the profile fallback.
M app/controllers/listenbrainz_listen_controller.rb => app/controllers/listenbrainz_listen_controller.rb +15 -2
@@ 11,8 11,17 @@ class ListenbrainzListenController < ApplicationController
            return
        end

        result = lb_api.get("/user/#{listenbrainz_username}/playing-now")
        listen = first_listen result
        listen = nil
        if show_pinned_recording?
            result = lb_api.get("/#{listenbrainz_username}/pins/current")
            listen = result&.dig('pinned_recording')
        end

        if !listen
            result = lb_api.get("/user/#{listenbrainz_username}/playing-now")
            listen = first_listen result
        end

        if !listen
            result = lb_api.get("/user/#{listenbrainz_username}/listens", {
                :count => 1,


@@ 30,6 39,10 @@ class ListenbrainzListenController < ApplicationController

    private

    def show_pinned_recording?
       user&.custom_fields&.[]('listenbrainz_show_pinned_recording') || false
    end

    def first_listen(result)
        result.dig('payload', 'listens')&.first
    end

D app/controllers/listenbrainz_pinned_recording_controller.rb => app/controllers/listenbrainz_pinned_recording_controller.rb +0 -19
@@ 1,19 0,0 @@
require 'cgi'
require_relative './listenbrainz_mixin'
require_relative '../../lib/helpers/listen_helper'

class ListenbrainzPinnedRecordingController < ApplicationController
    include ListenBrainzMixin

    def current_pin
        if !listenbrainz_enabled
            head :forbidden
            return
        end

        result = lb_api.get("/#{listenbrainz_username}/pins/current")
        listen = result&.dig('pinned_recording')
        ListenBrainz::ListenHelper::enhance_metadata(listen) if listen
        render :json => listen
    end
end

M assets/javascripts/discourse/components/listenbrainz-listening.js => assets/javascripts/discourse/components/listenbrainz-listening.js +37 -1
@@ 15,6 15,14 @@ export default class ListeningComponent extends Component {
    @tracked loading = true
    @tracked listen = null

    get isPlayingNow() {
        return !!this.listen?.playing_now
    }

    get isPin() {
        return !!this.listen?.pinned_until
    }

    get artist() {
        return this.artistCredits.reduce((result, c) => {
            result.push(c.artist_credit_name)


@@ 67,7 75,7 @@ export default class ListeningComponent extends Component {
    }

    get listenDateTime() {
        const timestamp = this.listen?.listened_at
        const timestamp = this.listen?.listened_at || this.listen?.created
        return timestamp ? new Date(timestamp * 1000) : null
    }



@@ 106,6 114,34 @@ export default class ListeningComponent extends Component {
        return !this.show && this.user.custom_fields.listenbrainz_display === 'listen_profile_fallback'
    }

    get hasIcon() {
        return this.isPlayingNow || this.isPin
    }

    get icon() {
        if (this.isPlayingNow) {
            return 'music'
        } else if (this.isPin) {
            return 'thumbtack'
        } else {
            return null
        }
    }

    get iconTooltip() {
        if (this.isPlayingNow) {
            return I18n.t('listenbrainz.listening.playing_now_tooltip')
        } else if (this.isPin) {
            return I18n.t('listenbrainz.listening.pin_tooltip')
        } else {
            return null
        }
    }

    get blurb() {
        return this.listen?.blurb_content
    }

    async loadNowPlaying() {
        this.loading = true
        try {

D assets/javascripts/discourse/components/listenbrainz-pinned-recording.js => assets/javascripts/discourse/components/listenbrainz-pinned-recording.js +0 -126
@@ 1,126 0,0 @@
import Component from '@glimmer/component'
import { tracked } from '@glimmer/tracking'
import { ajax } from 'discourse/lib/ajax'
import { buildMbUrl } from '../lib/listenbrainz-utils'

export default class ListeningComponent extends Component {

    constructor(owner, args) {
        super(owner, args)
        this.user = args.user
        this.loadCurrentPinPlaying()
    }

    @tracked show = true
    @tracked loading = true
    @tracked listen = null

    get artist() {
        return this.artistCredits.reduce((result, c) => {
            result.push(c.artist_credit_name)
            if (c.join_phrase) {
                result.push(c.join_phrase)
            }
            return result
        }, []).join('')
    }

    get title() {
        return this.listen?.track_metadata?.track_name
    }

    get album() {
        return this.listen?.track_metadata?.release_name
    }

    get blurb() {
        return this.listen?.blurb_content
    }

    get artistCredits() {
        const metadata = this.listen?.track_metadata
        let artists = metadata?.mbid_mapping?.artists
        if (!artists || artists.length === 0) {
            // No detailed artist credits, build a single credit from artist name
            const artistMbids = metadata?.additional_info?.artist_mbids ||
                metadata?.mbid_mapping?.artist_mbids
            artists = [{
                // Link only if there is exactly one artist
                artist_mbid: artistMbids?.length === 1 ? artistMbids[0] : null,
                artist_credit_name: metadata?.mbid_mapping?.artist_credit_name ||
                    metadata?.artist_name,
                join_phrase: '',
            }]
        }
        return artists
    }

    get duration() {
        const additional_info = this.listen?.track_metadata?.additional_info
        if (additional_info) {
            const duration = additional_info.duration_ms ?
                additional_info.duration_ms / 1000 :
                additional_info.duration
            if (duration) {
                const minutes = parseInt(duration / 60, 10)
                const seconds = String(parseInt(duration % 60, 10)).padStart(2, '0')
                return `${minutes}:${seconds}`
            }
        }
        return ''
    }

    get pinDateTime() {
        const timestamp = this.listen?.created
        return timestamp ? new Date(timestamp * 1000) : null
    }

    get recordingMbid() {
        const metadata = this.listen?.track_metadata
        return metadata?.additional_info?.recording_mbid ||
            metadata?.mbid_mapping?.recording_mbid
    }

    get releaseMbid() {
        const metadata = this.listen?.track_metadata
        return metadata?.additional_info?.release_mbid ||
            metadata?.mbid_mapping?.release_mbid
    }

    get recordingUrl() {
        return buildMbUrl('recording', this.recordingMbid)
    }

    get releaseUrl() {
        return buildMbUrl('release', this.releaseMbid)
    }

    get playUrl() {
        const recordingMbid = this.recordingMbid
        if (recordingMbid) {
            return `https://listenbrainz.org/player?recording_mbids=${encodeURIComponent(recordingMbid)}`
        }
    }

    get coverUrl() {
        return this.listen?.coverart_url
    }

    get enableProfileFallback() {
        return !this.show && this.user.custom_fields.listenbrainz_display === 'listen_profile_fallback'
    }

    async loadCurrentPinPlaying() {
        this.loading = true
        try {
            const listen = await ajax(`/listenbrainz/pin/${this.user.id}`)
            this.show = !!listen
            this.listen = listen
        } catch(err) {
            this.show = false
            console.error(err)
        } finally {
            this.loading = false
        }
    }
}

M assets/javascripts/discourse/components/listenbrainz-preferences.js => assets/javascripts/discourse/components/listenbrainz-preferences.js +0 -1
@@ 7,7 7,6 @@ import I18n from "I18n"
const displayOptions = [
    'profile',
    'listen',
    'pinned_recording',
    'listen_profile_fallback',
]


M assets/javascripts/discourse/templates/components/listenbrainz-listening.hbs => assets/javascripts/discourse/templates/components/listenbrainz-listening.hbs +8 -4
@@ 16,8 16,8 @@
            {{else}}
            <div class="listenbrainz-card-primary">
                <div class="listenbrainz-card-title" title="{{title}}">
                    {{#if listen.playing_now}}
                    <span title="{{i18n "listenbrainz.listening.playing_now_tooltip"}}">🎶</span>
                    {{#if hasIcon}}
                    <span title="{{iconTooltip}}">{{d-icon icon}}</span>
                    {{/if}}
                    {{#if recordingMbid}}
                    <a href="{{recordingUrl}}" target="_blank">{{title}}</a>


@@ 56,8 56,12 @@
            </div>
        </div>
    </div>
    {{#if blurb}}
    <div class="listenbrainz-card-additional-content">
        <blockquote>{{blurb}}</blockquote>
    </div>
    {{/if}}
</div>
{{/if}}
{{#if enableProfileFallback}}
{{else if enableProfileFallback}}
{{listenbrainz-profile user=user}}
{{/if}}

D assets/javascripts/discourse/templates/components/listenbrainz-pinned-recording.hbs => assets/javascripts/discourse/templates/components/listenbrainz-pinned-recording.hbs +0 -65
@@ 1,65 0,0 @@
{{#if show }}
<div class="listenbrainz-card listenbrainz-listening">
    <div class="listenbrainz-card-main-content">
        <div class="listenbrainz-card-thumbnail">
            {{#if releaseMbid}}
            <a href="{{releaseUrl}}" title="{{album}}" target="_blank">
                {{listenbrainz-cover src=coverUrl}}
            </a>
            {{else}}
                {{listenbrainz-cover src=coverUrl title=album}}
            {{/if}}
        </div>
        <div class="listenbrainz-card-details">
            {{#if loading }}
            {{i18n "listenbrainz.listening.loading"}}
            {{else}}
            <div class="listenbrainz-card-primary">
                <div class="listenbrainz-card-title" title="{{title}}">
                    <span title="{{i18n "listenbrainz.listening.pin_tooltip"}}">❤️</span>
                    {{#if recordingMbid}}
                    <a href="{{recordingUrl}}" target="_blank">{{title}}</a>
                    {{else}}
                    {{title}}
                    {{/if}}
                </div>
                <div class="listenbrainz-card-duration" title="{{i18n "listenbrainz.listening.duration_tooltip"}}">
                    {{duration}}
                </div>
            </div>
            <div class="listenbrainz-card-secondary" title="{{artist}}">
                {{listenbrainz-artist-credits artists=artistCredits}}
            </div>
            {{/if}}
        </div>
        <div class="listenbrainz-card-right-section">
            <div class="listenbrainz-card-timestamp">
                <span class="listenbrainz-card-listen-time" title="{{pinDateTime}}">
                    {{format-date pinDateTime leaveAgo="true" format="medium"}}
                </span>
            </div>
            <div class="listenbrainz-card-controls">
                {{#if recordingMbid}}
                <a class="btn-flat listenbrainz-play-button"
                    title="{{i18n "listenbrainz.listening.play_on_listenbrainz"}}"
                    href="{{playUrl}}"
                    target="_blank">
                    <svg viewBox="0 0 512 512">
                        <path fill="currentColor"
                            d="M371.7 238l-176-107c-15.8-8.8-35.7 2.5-35.7 21v208c0 18.4 19.8 29.8 35.7 21l176-101c16.4-9.1 16.4-32.8 0-42zM504 256C504 119 393 8 256 8S8 119 8 256s111 248 248 248 248-111 248-248zm-448 0c0-110.5 89.5-200 200-200s200 89.5 200 200-89.5 200-200 200S56 366.5 56 256z">
                        </path>
                    </svg>
                </a>
                {{/if}}
            </div>
        </div>
    </div>
    {{#if blurb}}
    <div class="listenbrainz-card-additional-content">
        <blockquote>{{blurb}}</blockquote>
    </div>
    {{/if}}
</div>
{{else if enableProfileFallback}}
{{listenbrainz-profile user=user}}
{{/if}}

M assets/javascripts/discourse/templates/components/listenbrainz-preferences.hbs => assets/javascripts/discourse/templates/components/listenbrainz-preferences.hbs +4 -0
@@ 49,6 49,10 @@
    {{/if}}

    {{#if showListenOptions}}
    {{preference-checkbox
        labelKey="listenbrainz.preferences.show_pinned_recording"
        checked=user.custom_fields.listenbrainz_show_pinned_recording}}

    <div class="controls">
        <label>
            {{i18n "listenbrainz.preferences.listen_max_age_label"}}

M assets/javascripts/discourse/templates/connectors/user-card-after-metadata/listenbrainz-listening.hbs => assets/javascripts/discourse/templates/connectors/user-card-after-metadata/listenbrainz-listening.hbs +0 -2
@@ 1,8 1,6 @@
{{#if user.custom_fields.listenbrainz_enable}}
    {{#if (eq user.custom_fields.listenbrainz_display 'profile')}}
    {{listenbrainz-profile user=user}}
    {{else if (eq user.custom_fields.listenbrainz_display 'pinned_recording')}}
    {{listenbrainz-pinned-recording user=user}}
    {{else}}
    {{listenbrainz-listening user=user}}
    {{/if}}

M assets/javascripts/discourse/templates/connectors/user-profile-primary/listenbrainz-listening.hbs => assets/javascripts/discourse/templates/connectors/user-profile-primary/listenbrainz-listening.hbs +0 -2
@@ 1,8 1,6 @@
{{#if model.custom_fields.listenbrainz_enable}}
    {{#if (eq model.custom_fields.listenbrainz_display 'profile')}}
    {{listenbrainz-profile user=model}}
    {{else if (eq model.custom_fields.listenbrainz_display 'pinned_recording')}}
    {{listenbrainz-pinned-recording user=model}}
    {{else}}
    {{listenbrainz-listening user=model}}
    {{/if}}

M assets/stylesheets/listenbrainz.scss => assets/stylesheets/listenbrainz.scss +4 -0
@@ 91,6 91,10 @@ aside.onebox.listenbrainzprofile {
                    -webkit-box-orient: vertical;
                    overflow: hidden;
                    text-overflow: ellipsis;

                    .d-icon {
                        color: var(--tertiary);
                    }
                }

                .listenbrainz-card-duration {

M config/locales/client.de.yml => config/locales/client.de.yml +2 -1
@@ 4,6 4,7 @@ de:
      profile_link_tooltip: '{{username}}s Profil auf ListenBrainz'
      listening:
        loading: Lade Listen…
        pin_tooltip: Angeheftete Aufnahme
        playing_now_tooltip: Hört gerade
        play_on_listenbrainz: Spiele auf ListenBrainz
        duration_tooltip: "Spieldauer"


@@ 25,7 26,6 @@ de:
        display_options:
          listen: Zuletzt gehört
          profile: Benutzerprofil-Statistiken
          pinned_recording: Angeheftete Aufnahme
          listen_profile_fallback: Zuletzt gehört, fällt zurück auf Benutzerprofil-Statistiken
        range_label: Zeitraum
        range_options:


@@ 33,6 33,7 @@ de:
          this_month: Diesen Monat
          this_year: Dieses Jahr
          all_time: Insgesamt
        show_pinned_recording: Angeheftete Aufnahme statt letztem Listen anzeigen, falls verfügbar
        listen_max_age_label: Kürzliches Listen nur anzeigen, wenn nicht älter als
        listen_max_age_options:
          0: Nur "hört gerade"

M config/locales/client.en.yml => config/locales/client.en.yml +2 -1
@@ 4,6 4,7 @@ en:
      profile_link_tooltip: "{{username}}’s profile on ListenBrainz"
      listening:
        loading: "Loading listen…"
        pin_tooltip: "Pinned recording"
        playing_now_tooltip: "Currently listening"
        play_on_listenbrainz: "Play on ListenBrainz"
        duration_tooltip: "Duration"


@@ 25,7 26,6 @@ en:
        display_options:
          listen: "Most recent listen"
          profile: "User profile statistics"
          pinned_recording: "Pinned recording"
          listen_profile_fallback: "Most recent listen with fallback to user profile statistics"
        range_label: "Time range"
        range_options:


@@ 34,6 34,7 @@ en:
          this_year: "This year"
          all_time: "All time"
        range_fallback_label: "Fallback to larger time range if there are no listens in the selected range"
        show_pinned_recording: "Show pinned recording instead of recent listen, if available"
        listen_max_age_label: "Show recent listen not older than"
        listen_max_age_options:
          0: "Only now playing"

M plugin.rb => plugin.rb +8 -2
@@ 45,17 45,23 @@ after_initialize do
    register_editable_user_custom_field :listenbrainz_profile_range_fallback
    DiscoursePluginRegistry.serialized_current_user_fields << 'listenbrainz_profile_range_fallback'

    User.register_custom_field_type 'listenbrainz_show_pinned_recording', :boolean
    register_editable_user_custom_field :listenbrainz_show_pinned_recording
    DiscoursePluginRegistry.serialized_current_user_fields << 'listenbrainz_show_pinned_recording'

    User.register_custom_field_type 'listenbrainz_max_listen_age_hours', :int
    register_editable_user_custom_field :listenbrainz_max_listen_age_hours
    DiscoursePluginRegistry.serialized_current_user_fields << 'listenbrainz_max_listen_age_hours'

    # Extra icon
    register_svg_icon 'music'
    register_svg_icon 'thumbtack'

    # Register routes
    require_relative "app/controllers/listenbrainz_listen_controller"
    require_relative "app/controllers/listenbrainz_pinned_recording_controller"
    require_relative "app/controllers/listenbrainz_profile_controller"
    Discourse::Application.routes.append do
        get "listenbrainz/now-playing/:user_id" => "listenbrainz_listen#now_playing"
        get "listenbrainz/pin/:user_id" => "listenbrainz_pinned_recording#current_pin"
        get "listenbrainz/profile/:user_id" => "listenbrainz_profile#profile"
    end
end