forked from Mirrors/swingmusic-webclient
lastfm integration
This commit is contained in:
@@ -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",
|
||||
|
||||
11
src/assets/icons/lastfm.svg
Normal file
11
src/assets/icons/lastfm.svg
Normal 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 |
80
src/components/SettingsView/Components/SecretInput.vue
Normal file
80
src/components/SettingsView/Components/SecretInput.vue
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
22
src/context_menus/hashing.ts
Normal file
22
src/context_menus/hashing.ts
Normal 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');
|
||||
}
|
||||
@@ -102,4 +102,7 @@ export interface DBSettings {
|
||||
scanInterval: number
|
||||
plugins: Plugin[];
|
||||
version: string;
|
||||
lastfmApiKey: string;
|
||||
lastfmApiSecret: string;
|
||||
lastfmSessionKey: string;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
61
src/settings/plugins/lastfm.ts
Normal file
61
src/settings/plugins/lastfm.ts
Normal 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]
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user