~phw/discourse-listenbrainz

80f93b1dd4548a6cff8aa73819e770a35ae446cd — Philipp Wolfer 9 months ago eaff604
Pinned recording card

Implements #7
A app/controllers/listenbrainz_pinned_recording_controller.rb => app/controllers/listenbrainz_pinned_recording_controller.rb +19 -0
@@ 0,0 1,19 @@
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

A assets/javascripts/discourse/components/listenbrainz-pinned-recording.js => assets/javascripts/discourse/components/listenbrainz-pinned-recording.js +126 -0
@@ 0,0 1,126 @@
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 +6 -1
@@ 4,7 4,12 @@ import { action } from '@ember/object'
import { getOwner } from "discourse-common/lib/get-owner"
import I18n from "I18n"

const displayOptions = ['profile', 'listen', 'listen_profile_fallback']
const displayOptions = [
    'profile',
    'listen',
    'pinned_recording',
    'listen_profile_fallback',
]

const rangeOptions = [
    'this_week',

A assets/javascripts/discourse/templates/components/listenbrainz-pinned-recording.hbs => assets/javascripts/discourse/templates/components/listenbrainz-pinned-recording.hbs +65 -0
@@ 0,0 1,65 @@
{{#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/connectors/user-card-after-metadata/listenbrainz-listening.hbs => assets/javascripts/discourse/templates/connectors/user-card-after-metadata/listenbrainz-listening.hbs +2 -0
@@ 1,6 1,8 @@
{{#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 +2 -0
@@ 1,6 1,8 @@
{{#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 +26 -2
@@ 6,6 6,7 @@ aside.onebox.listenbrainzprofile {
    --secondary-text-color: var(--primary-800);
    --background-color: var(--secondary);
    --border-color: var(--secondary-high);
    --large-width: 480px;

    clear: both;
    background: var(--background-color);


@@ 23,7 24,7 @@ aside.onebox.listenbrainzprofile {
        color: var(--text-color);
        padding: 10px;

        @media (min-width: 480px) {
        @media (min-width: var(--large-width)) {
            padding: 15px;
        }



@@ 45,7 46,7 @@ aside.onebox.listenbrainzprofile {
            background-size: 100%;
            border-radius: 7px 0 0 7px;

            @media (min-width: 480px) {
            @media (min-width: var(--large-width)) {
                margin: -15px 1em -15px -15px;
            }



@@ 153,6 154,29 @@ aside.onebox.listenbrainzprofile {
            }
        }
    }

    .listenbrainz-card-additional-content {
        border-top: 1px solid var(--secondary-high);

        blockquote {
            border: none;
            margin: 0;
            background-color: inherit;
            padding: 10px;

            @media (min-width: var(--large-width)) {
                padding: 15px;
            }

            &::before {
                content: "“";
            }

            &::after {
                content: "”";
            }
        }
    }
}

// Hide LB card on collapsed user profile

M config/locales/client.de.yml => config/locales/client.de.yml +1 -0
@@ 25,6 25,7 @@ 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:

M config/locales/client.en.yml => config/locales/client.en.yml +1 -0
@@ 25,6 25,7 @@ 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:

M plugin.rb => plugin.rb +2 -0
@@ 51,9 51,11 @@ after_initialize do

    # 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