lastfm integration

This commit is contained in:
cwilvx
2024-12-30 20:58:46 +03:00
parent 56d1c9da90
commit 56b1ab35d3
14 changed files with 1094 additions and 33 deletions

View File

@@ -45,6 +45,7 @@
"typescript": "^5.0.4",
"vite": "^3.0.4",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.16.4",
"vite-plugin-singlefile": "^0.13.5",
"vite-svg-loader": "^4.0.0",

View File

@@ -0,0 +1,11 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg fill="currentColor" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path d="M14.131 22.948l-1.172-3.193c0 0-1.912 2.131-4.771 2.131-2.537 0-4.333-2.203-4.333-5.729 0-4.511 2.276-6.125 4.515-6.125 3.224 0 4.245 2.089 5.125 4.772l1.161 3.667c1.161 3.561 3.365 6.421 9.713 6.421 4.548 0 7.631-1.391 7.631-5.068 0-2.968-1.697-4.511-4.844-5.244l-2.344-0.511c-1.624-0.371-2.104-1.032-2.104-2.131 0-1.249 0.985-1.984 2.604-1.984 1.767 0 2.704 0.661 2.865 2.24l3.661-0.444c-0.297-3.301-2.584-4.656-6.323-4.656-3.308 0-6.532 1.251-6.532 5.245 0 2.5 1.204 4.077 4.245 4.807l2.484 0.589c1.865 0.443 2.484 1.224 2.484 2.287 0 1.359-1.323 1.921-3.828 1.921-3.703 0-5.244-1.943-6.124-4.625l-1.204-3.667c-1.541-4.765-4.005-6.531-8.891-6.531-5.287-0.016-8.151 3.385-8.151 9.192 0 5.573 2.864 8.595 8.005 8.595 4.14 0 6.125-1.943 6.125-1.943z"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,80 @@
<template>
<form class="secretinput" @submit.prevent="$emit('submit', input)">
<div class="left rounded-sm no-scroll">
<input :type="showText ? 'text' : 'password'" v-model="input" @input="() => (showTextManual = true)" />
<button @click.prevent="showTextManual = !showTextManual">
<EyeSvg v-if="showText" />
<EyeSlashSvg v-else />
</button>
</div>
<div class="right">
<button>Save</button>
</div>
</form>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import EyeSvg from '@/assets/icons/eye.svg'
import EyeSlashSvg from '@/assets/icons/eye.slash.svg'
const props = defineProps<{
text: string
}>()
const input = ref('')
const showTextManual = ref(false)
const showText = computed(() => {
if (showTextManual.value) return true
return input.value.length == 0
})
defineEmits<{
(e: 'submit', value: string): void
}>()
onMounted(() => {
if (props.text) {
input.value = props.text
}
})
</script>
<style lang="scss">
.secretinput {
display: grid;
grid-template-columns: 1fr max-content;
gap: 1rem;
width: 100%;
.left {
display: flex;
align-items: center;
gap: 1rem;
position: relative;
background-color: $gray5;
input {
height: 100%;
width: 100%;
border: none;
outline: none;
background: none;
padding: $small;
font-size: 12px;
font-family: 'SF Mono';
color: #ffffff00;
}
svg {
height: 1rem;
}
button {
background: none;
}
}
}
</style>

View File

@@ -84,6 +84,11 @@
component_key="streaming_quality"
/>
<BackupRestore v-if="setting.type === SettingType.backup" />
<SecretInput
v-if="setting.type === SettingType.secretinput"
:text="setting.state ? setting.state() : ''"
@submit="setting.action"
/>
</div>
</div>
</div>
@@ -107,6 +112,7 @@ import Pairing from '../modals/settings/custom/Pairing.vue'
import DropDown from '../shared/DropDown.vue'
import About from './About.vue'
import BackupRestore from './Components/BackupRestore.vue'
import SecretInput from './Components/SecretInput.vue'
defineProps<{
group: SettingGroup

View File

@@ -59,7 +59,7 @@ const currentGroup = computed(() => {
// select default tab
for (const group of settingGroups) {
for (const settings of group.groups) {
if (settings.title === 'Appearance') {
if (settings.title === 'Last.fm') {
return settings
}
}

View File

@@ -0,0 +1,22 @@
import crypto from 'crypto';
export function getLastFmApiSig(data: {[key: string]: any}, secret: string): string {
// Sort keys alphabetically
const sortedKeys = Object.keys(data).sort();
// Concatenate parameters in name+value format
const concatenatedString = sortedKeys.reduce((acc, key) => {
// Ensure values are properly encoded
const value = encodeURIComponent(data[key].toString());
return acc + key + value;
}, '');
// Append secret
const stringToHash = concatenatedString + secret;
// Generate MD5 hash
return crypto.createHash('md5')
.update(stringToHash)
.digest('hex');
}

View File

@@ -102,4 +102,7 @@ export interface DBSettings {
scanInterval: number
plugins: Plugin[];
version: string;
lastfmApiKey: string;
lastfmApiSecret: string;
lastfmSessionKey: string;
}

View File

@@ -20,7 +20,7 @@ export function getBaseUrl() {
axios.defaults.baseURL = getBaseUrl()
export default async (args: FetchProps) => {
export default async (args: FetchProps, withCredentials: boolean = true) => {
const on_ngrok = args.url.includes('ngrok')
const ngrok_config = {
'ngrok-skip-browser-warning': 'stupid-SOAB!',
@@ -37,7 +37,7 @@ export default async (args: FetchProps) => {
method: args.method || 'POST',
// INFO: Add ngrok header and provided headers
headers: { ...args.headers, ...(on_ngrok ? ngrok_config : {}) },
withCredentials: true,
withCredentials: withCredentials,
})
stopLoading()

View File

@@ -7,7 +7,7 @@ export enum SettingType {
root_dirs,
free_number_input,
locked_number_input,
// custom components 👇
quick_actions,
profile,
@@ -15,5 +15,6 @@ export enum SettingType {
pairing,
about,
streaming_quality,
backup
backup,
secretinput,
}

View File

@@ -1,20 +1,29 @@
import lyrics from "./lyrics";
import useAuth from "@/stores/auth";
import { SettingCategory } from "@/interfaces/settings";
import lyrics from './lyrics'
import useAuth from '@/stores/auth'
import { SettingCategory } from '@/interfaces/settings'
import LyricsSvg from "@/assets/icons/lyrics.svg?raw";
import { loggedInUserIsAdmin } from "../utils";
import LyricsSvg from '@/assets/icons/lyrics.svg?raw'
import LastfmSvg from '@/assets/icons/lastfm.svg?raw'
import { loggedInUserIsAdmin } from '../utils'
import lastfm from './lastfm'
export default <SettingCategory>{
title: "Plugins",
show_if: loggedInUserIsAdmin,
groups: [
{
title: "Lyrics",
icon: LyricsSvg,
desc: "Finds and displays lyrics from the internet.",
settings: lyrics,
experimental: true,
},
],
};
title: 'Plugins',
show_if: loggedInUserIsAdmin,
groups: [
{
title: 'Lyrics',
icon: LyricsSvg,
desc: 'Finds and displays lyrics from the internet.',
settings: lyrics,
experimental: true,
},
{
title: 'Last.fm',
icon: LastfmSvg,
desc: 'Last.fm integration',
settings: lastfm,
},
],
}

View File

@@ -0,0 +1,61 @@
import useSettings from '@/stores/settings'
import { Setting } from '@/interfaces/settings'
import { SettingType } from '../enums'
const authorize = <Setting>{
title: 'Connect your account',
desc: 'Allow Swing Music to access your Last.fm account',
type: SettingType.button,
action: () => {
const settings = useSettings()
if (settings.lastfm_integration_started) {
return settings.finishLastfmAuth()
}
if (settings.lastfm_session_key) {
return settings.disconnectLastfm()
}
return settings.authorizeLastfmApiKey()
},
button_text: () => {
const settings = useSettings()
if (settings.lastfm_integration_started) {
return 'Finish'
}
if (settings.lastfm_session_key) {
return 'Disconnect'
}
return 'Connect'
},
}
// const api_key = <Setting>{
// title: 'Use custom API Key',
// desc: 'instead of the Swing Music default to authenticate with Last.fm',
// type: SettingType.secretinput,
// state: () => useSettings().lastfm_api_key,
// action: (value: string) => {
// if (!value) {
// return
// }
// return useSettings().setLastfmApiKey(value)
// },
// }
// const api_secret = <Setting>{
// title: 'Use custom API Secret',
// desc: 'instead of the Swing Music default to sign your scrobble submission',
// type: SettingType.secretinput,
// state: () => useSettings().lastfm_api_secret,
// action: (value: string) => {
// if (!value) {
// return
// }
// return useSettings().setLastfmApiSecret(value)
// },
// }
export default [authorize]

View File

@@ -7,6 +7,9 @@ import { pluginSetActive, updatePluginSettings } from '@/requests/plugins'
import { updateConfig } from '@/requests/settings'
import { usePlayer } from '@/stores/player'
import { content_width } from '../content-width'
import { getLastFmApiSig } from '@/context_menus/hashing'
import useAxios from '@/requests/useAxios'
import { paths } from '@/config'
export default defineStore('settings', {
state: () => ({
@@ -47,6 +50,11 @@ export default defineStore('settings', {
auto_download: false,
overide_unsynced: false,
},
lasftfm_token: '',
lastfm_api_key: '',
lastfm_api_secret: '',
lastfm_session_key: '',
lastfm_integration_started: false,
// audio
use_silence_skip: true,
@@ -80,6 +88,9 @@ export default defineStore('settings', {
this.periodicInterval = settings.scanInterval
this.enableWatchDog = settings.enableWatchDog
this.lastfm_api_key = settings.lastfmApiKey
this.lastfm_api_secret = settings.lastfmApiSecret
this.lastfm_session_key = settings.lastfmSessionKey
this.use_lyrics_plugin = settings.plugins.find(p => p.name === 'lyrics_finder')?.active
if (this.use_lyrics_plugin) {
@@ -224,11 +235,11 @@ export default defineStore('settings', {
},
async genericToggleSetting(key: string, value: any, prop: string) {
// @ts-expect-error
const oldValue = this[prop]
// @ts-expect-error
this[prop] = value
console.log(this[prop])
const res = await updateConfig(key, value)
if (res.status !== 200) {
@@ -282,6 +293,67 @@ export default defineStore('settings', {
'show_albums_as_singles'
)
},
async setLastfmApiKey(key: string) {
return await this.genericToggleSetting('lastfmApiKey', key, 'lastfm_api_key')
},
async setLastfmApiSecret(key: string) {
return await this.genericToggleSetting('lastfmApiSecret', key, 'lastfm_api_secret')
},
async authorizeLastfmApiKey() {
const getTokenUrl =
'http://ws.audioscrobbler.com/2.0/?format=json&method=auth.getToken&api_key=' +
this.lastfm_api_key +
'&api_sig=' +
getLastFmApiSig({ api_key: this.lastfm_api_key }, this.lastfm_api_secret)
const data = await useAxios(
{
url: getTokenUrl,
method: 'POST',
},
false
)
console.log('res: ', data)
if (data.status !== 200) {
return
}
this.lasftfm_token = data.data.token
const url = 'https://www.last.fm/api/auth/?api_key=' + this.lastfm_api_key + '&token=' + this.lasftfm_token
window.open(url, '_blank')
this.lastfm_integration_started = true
},
async finishLastfmAuth() {
const res = await useAxios({
url: paths.api.plugins + '/lastfm/session/create',
method: 'POST',
props: {
token: this.lasftfm_token,
},
})
console.log('res: ', res)
if (res.status !== 200) {
return
}
this.lastfm_session_key = res.data.session_key
this.lastfm_integration_started = false
},
async disconnectLastfm() {
const res = await useAxios({
url: paths.api.plugins + '/lastfm/session/delete',
method: 'POST',
})
if (res.status !== 200) {
return
}
this.lastfm_session_key = ''
},
setStreamingQuality(quality: string) {
this.streaming_quality = quality
},

View File

@@ -5,6 +5,7 @@ import vue from "@vitejs/plugin-vue";
import svgLoader from "vite-svg-loader";
import { VitePWA } from "vite-plugin-pwa";
import viteCompression from "vite-plugin-compression";
import { nodePolyfills } from 'vite-plugin-node-polyfills'
const path = require("path");
@@ -90,6 +91,9 @@ export default defineConfig({
viteCompression({
threshold: 150,
}),
nodePolyfills({
include: ['crypto'],
}),
],
resolve: {
alias: {

809
yarn.lock
View File

File diff suppressed because it is too large Load Diff