├── .gitattributes ├── .gitignore ├── .prettierrc ├── README.md ├── dist └── copytoclipboard.js ├── docs ├── after.png ├── marketplace.png └── settings.png ├── manifest.json ├── package.json ├── src ├── app.tsx ├── settings.json └── types │ ├── css-modules.d.ts │ ├── spicetify.d.ts │ └── types.d.ts ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/* linguist-vendored -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional stylelint cache 63 | .stylelintcache 64 | 65 | # Microbundle cache 66 | .rpt2_cache/ 67 | .rts2_cache_cjs/ 68 | .rts2_cache_es/ 69 | .rts2_cache_umd/ 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variable files 81 | .env 82 | .env.development.local 83 | .env.test.local 84 | .env.production.local 85 | .env.local 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | .parcel-cache 90 | 91 | # Next.js build output 92 | .next 93 | out 94 | 95 | # Nuxt.js build / generate output 96 | .nuxt 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit build / generate output 142 | .svelte-kit 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/node 145 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSameLine": true, 4 | "bracketSpacing": false, 5 | "endOfLine": "lf", 6 | "printWidth": 80, 7 | "semi": true, 8 | "singleQuote": true, 9 | "trailingComma": "all" 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Copy text extension 2 | 3 | Spicetify extension which allows you to copy text (song name, artist name, album name) in Spotify. 4 | 5 | ## Download from Marketplace 6 | 7 |
8 | 9 |
10 | 11 | ## Manual Installation 12 | 13 | Download [copytoclipboard.js](./dist/copytoclipboard.js) in `dist` folder and follow this [guide](https://spicetify.app/docs/advanced-usage/extensions#installing). 14 | 15 | ## Settings 16 | 17 | You can change separator between song name and artist names at bottom of Settings page 18 | 19 | ![settings](docs/settings.png) 20 | 21 | ![after](docs/after.png) 22 | 23 | ## Note 24 | 25 | If you installed from marketplace and it suddenly isn't working, reinstall it (https://github.com/pnthach95/spicetify-extensions/issues/16) 26 | 27 | If you encounter any errors, please report with link to the item you want to copy. 28 | -------------------------------------------------------------------------------- /dist/copytoclipboard.js: -------------------------------------------------------------------------------- 1 | !async function(){for(;!Spicetify.React||!Spicetify.ReactDOM;)await new Promise(e=>setTimeout(e,10));var o,c,l,p,d,u,e,t,a,n,i,f,m,s,r;o=Object.create,c=Object.defineProperty,l=Object.getOwnPropertyDescriptor,p=Object.getOwnPropertyNames,d=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,a=(e=(e,t)=>function(){return t||(0,e[p(e)[0]])((t={exports:{}}).exports,t),t.exports})({"external-global-plugin:react-dom"(e,t){t.exports=Spicetify.ReactDOM}}),n=(t=(e,t,a)=>{a=null!=e?o(d(e)):{};var i=!t&&e&&e.__esModule?a:c(a,"default",{value:e,enumerable:!0}),s=e,r=void 0,n=void 0;if(s&&"object"==typeof s||"function"==typeof s)for(let e of p(s))u.call(i,e)||e===r||c(i,e,{get:()=>s[e],enumerable:!(n=l(s,e))||n.enumerable});return i})(e({"external-global-plugin:react"(e,t){t.exports=Spicetify.React}})()),i=t(a()),f=class{constructor(e,t,a={}){this.name=e,this.settingsId=t,this.initialSettingsFields=a,this.settingsFields=this.initialSettingsFields,this.setRerender=null,this.pushSettings=async()=>{for(Object.entries(this.settingsFields).forEach(([e,t])=>{"button"!==t.type&&void 0===this.getFieldValue(e)&&this.setFieldValue(e,t.defaultValue)});!Spicetify?.Platform?.History?.listen;)await new Promise(e=>setTimeout(e,100));this.stopHistoryListener&&this.stopHistoryListener(),this.stopHistoryListener=Spicetify.Platform.History.listen(e=>{"/preferences"===e.pathname&&this.render()}),"/preferences"===Spicetify.Platform.History.location.pathname&&await this.render()},this.rerender=()=>{this.setRerender&&this.setRerender(Math.random())},this.render=async()=>{for(;!document.getElementById("desktop.settings.selectLanguage");){if("/preferences"!==Spicetify.Platform.History.location.pathname)return;await new Promise(e=>setTimeout(e,100))}var e=document.querySelector(".main-view-container__scroll-node-child main div");if(!e)return console.error("[spcr-settings] settings container not found");let t=Array.from(e.children).find(e=>e.id===this.settingsId);t?console.log(t):((t=document.createElement("div")).id=this.settingsId,e.appendChild(t)),i.default.render(n.default.createElement(this.FieldsContainer,null),t)},this.addButton=(e,t,a,i,s)=>{this.settingsFields[e]={type:"button",description:t,value:a,events:{onClick:i,...s}}},this.addInput=(e,t,a,i,s,r)=>{this.settingsFields[e]={type:"input",description:t,defaultValue:a,inputType:s,events:{onChange:i,...r}}},this.addHidden=(e,t)=>{this.settingsFields[e]={type:"hidden",defaultValue:t}},this.addToggle=(e,t,a,i,s)=>{this.settingsFields[e]={type:"toggle",description:t,defaultValue:a,events:{onChange:i,...s}}},this.addDropDown=(e,t,a,i,s,r)=>{this.settingsFields[e]={type:"dropdown",description:t,defaultValue:a[i],options:a,events:{onSelect:s,...r}}},this.getFieldValue=e=>JSON.parse(Spicetify.LocalStorage.get(this.settingsId+"."+e)||"{}")?.value,this.setFieldValue=(e,t)=>{Spicetify.LocalStorage.set(this.settingsId+"."+e,JSON.stringify({value:t}))},this.FieldsContainer=()=>{var[e,t]=(0,n.useState)(0);return this.setRerender=t,n.default.createElement("div",{className:"x-settings-section",key:e},n.default.createElement("h2",{className:"TypeElement-cello-textBase-type"},this.name),Object.entries(this.settingsFields).map(([e,t])=>n.default.createElement(this.Field,{nameId:e,field:t})))},this.Field=a=>{var e=this.settingsId+"."+a.nameId;let t;if(t="button"===a.field.type?a.field.value:this.getFieldValue(a.nameId),"hidden"===a.field.type)return n.default.createElement(n.default.Fragment,null);const[i,s]=(0,n.useState)(t),r=e=>{void 0!==e&&(s(e),this.setFieldValue(a.nameId,e))};return n.default.createElement("div",{className:"x-settings-row"},n.default.createElement("div",{className:"x-settings-firstColumn"},n.default.createElement("label",{className:"TypeElement-viola-textSubdued-type",htmlFor:e},a.field.description||"")),n.default.createElement("div",{className:"x-settings-secondColumn"},"input"===a.field.type?n.default.createElement("input",{className:"x-settings-input",id:e,dir:"ltr",value:i,type:a.field.inputType||"text",...a.field.events,onChange:e=>{r(e.currentTarget.value);var t=a.field.events?.onChange;t&&t(e)}}):"button"===a.field.type?n.default.createElement("span",null,n.default.createElement("button",{id:e,className:"Button-sc-y0gtbx-0 Button-small-buttonSecondary-useBrowserDefaultFocusStyle x-settings-button",...a.field.events,onClick:e=>{r();var t=a.field.events?.onClick;t&&t(e)},type:"button"},i)):"toggle"===a.field.type?n.default.createElement("label",{className:"x-settings-secondColumn x-toggle-wrapper"},n.default.createElement("input",{id:e,className:"x-toggle-input",type:"checkbox",checked:i,...a.field.events,onClick:e=>{r(e.currentTarget.checked);var t=a.field.events?.onClick;t&&t(e)}}),n.default.createElement("span",{className:"x-toggle-indicatorWrapper"},n.default.createElement("span",{className:"x-toggle-indicator"}))):"dropdown"===a.field.type?n.default.createElement("select",{className:"main-dropDown-dropDown",id:e,...a.field.events,onChange:e=>{r(a.field.options[e.currentTarget.selectedIndex]);var t=a.field.events?.onChange;t&&t(e)}},a.field.options.map((e,t)=>n.default.createElement("option",{selected:e===i,value:t+1},e))):n.default.createElement(n.default.Fragment,null)))}}},s={ru:{error:"Ошибка",text:"Скопировать текст",songAndArtist:"Cкопировать трек и артиста",copied:"Скопировано",copyImage:"Ссылка на изображение",settings:{name:"Copy to clipboard settings",separator:"Separator between Song name and Artist names"}},en:{error:"Error",text:"Copy Text",songAndArtist:"Copy Song & Artist names",copied:"Copied",copyImage:"Copy image link",settings:{name:(m={ID:"settings-copy-to-clipboard",NAME:"Copy to clipboard settings",SEPARATOR:{KEY:"ctc-separator",DESCRIPTION:"Separator between Song name and Artist names",DEFAULT:"; "}}).NAME,separator:m.SEPARATOR.DESCRIPTION}},vi:{copied:"Đã sao chép",copyImage:"Sao chép liên kết ảnh",error:"Lỗi",settings:{name:"Cài đặt Copy to clipboard",separator:"Phân cách giữa tên bài hát và tên nghệ sĩ"},songAndArtist:"Sao chép tên bài hát & nghệ sĩ",text:"Sao chép tên"}},r=async function(){for(;!Spicetify||"complete"!==document.readyState;)await new Promise(e=>setTimeout(e,1e3));await new Promise(e=>setTimeout(e,1e3)),e=Spicetify.Locale?Spicetify.Locale.getLocale():"en";var e=await(Object.keys(s).includes(e)?s[e]:s.en),t=new f(e.settings.name,m.ID);t.addInput(m.SEPARATOR.KEY,e.settings.separator,m.SEPARATOR.DEFAULT),t.pushSettings(),E(e)},(async()=>{await r()})();async function y(e,t){try{var a=await Spicetify.CosmosAsync.get("https://api.spotify.com/v1/albums/"+t);return"image"===e?a.images[0].url:a.name}catch(e){throw console.log(e),new Error(e.message)}}async function g(e,t){var a=Spicetify.GraphQL.Definitions["queryArtistOverview"];try{var i=Spicetify.Locale?Spicetify.Locale.getLocale():"en",s=(await Spicetify.GraphQL.Request(a,{uri:t,includePrerelease:!1,locale:i,offset:0,limit:10}))["data"];if("image"!==e)return s.artistUnion.profile.name;var r=s.artistUnion.headerImage?.data?.sources?.[0]?.url;if(r)return r;throw new Error("No images")}catch(e){throw console.log(e),new Error(e.message)}}async function h(e){var t=Spicetify.GraphQL.Definitions["getTrackName"];try{var a=(await Spicetify.GraphQL.Request(t,{uri:e,offset:0,limit:10}))["data"];return a.trackUnion.name}catch(e){throw console.log(e),new Error(e.message)}}function S(e){if(e.includes("mosaic:"))throw new Error("Cannot copy mosaic image");if(e.startsWith("spotify")){var t=e.split(":").pop();if(t)return"https://i.scdn.co/image/"+t;throw new Error("Not found")}return e}async function w(e,t){try{var a=await Spicetify.CosmosAsync.get("sp://core-playlist/v1/playlist/spotify:playlist:"+t);return"name"===e?a.playlist.name:S(a.playlist.picture)}catch(e){throw console.log(e),new Error(e.message)}}async function v(e,t){try{var a=await Spicetify.CosmosAsync.get(`sp://core-show/v1/shows/${t}?responseFormat=protobufJson`);return"name"===e?a.header.showMetadata.name:S(a.header.showMetadata.covers.xlargeLink)}catch(e){throw console.log(e),new Error(e.message)}}async function I(e,t){try{var a=await Spicetify.Platform.ShowAPI.getEpisodeOrChapter("spotify:episode:"+t);return"name"===e?a.name:S(a.coverArt.reduce((e,t)=>e.width>t.width?e:t,{width:0,height:0,url:""}).url)}catch(e){throw console.log(e),new Error(e.message)}}async function A(e,t){try{var a=await Spicetify.CosmosAsync.get("sp://core-profile/v1/profiles",{usernames:t});return"name"===e?a.profiles[0].name:a.profiles[0].images.reduce((e,t)=>e.maxWidth>t.maxWidth?e:t,{maxWidth:0,maxHeight:0,url:""}).url}catch(e){throw console.log(e),new Error(e.message)}}function E(r){function n(e){e&&(Spicetify.showNotification(r.copied+": "+e),Spicetify.Platform.ClipboardAPI.copy(e))}new Spicetify.ContextMenu.Item(r.text,async e=>{var t=Spicetify.URI["Type"],a=Spicetify.URI.fromString(e[0]);const i=a._base62Id||a.id;try{switch(a.type){case t.TRACK:n(await h(a.toURI()));break;case t.LOCAL:var s=[];a.track&&s.push(a.track),a.artist&&s.push(a.artist),a.album&&s.push(a.album),n(s.join("; "));break;case t.LOCAL_ARTIST:n(""+(a.artist||""));break;case t.LOCAL_ALBUM:n(""+(a.album||""));break;case t.ALBUM:n(await y("name",a.id));break;case t.ARTIST:n(await g("name",a.toURI()));break;case t.PLAYLIST:case t.PLAYLIST_V2:n(await w("name",i));break;case t.SHOW:n(await v("name",i));break;case t.EPISODE:n(await I("name",i));break;case t.PROFILE:n(await A("name",a.username));break;case t.FOLDER:n((await Spicetify.Platform.RootlistAPI.getContents()).items.filter(e=>"folder"===e.type&&e.uri.includes(i))[0].name)}}catch(e){Spicetify.showNotification(r.error+": "+e.message)}},e=>{if(1!==e.length)return!1;var t=Spicetify.URI["Type"];switch(Spicetify.URI.fromString(e[0]).type){case t.TRACK:case t.LOCAL:case t.LOCAL_ARTIST:case t.LOCAL_ALBUM:case t.ALBUM:case t.ARTIST:case t.PLAYLIST:case t.PLAYLIST_V2:case t.SHOW:case t.EPISODE:case t.PROFILE:case t.FOLDER:return!0;default:return!1}},"copy").register(),new Spicetify.ContextMenu.Item(r.songAndArtist,async e=>{var t,a,i,s=Spicetify.URI["Type"],e=Spicetify.URI.fromString(e[0]);try{e.type===s.TRACK&&([t,a]=await Promise.allSettled([h(e.toURI()),async function(e){var t=Spicetify.GraphQL.Definitions["queryTrackArtists"];try{var a=(await Spicetify.GraphQL.Request(t,{uri:e,offset:0,limit:10}))["data"];return a.trackUnion.artists.items.map(e=>e.profile.name).join(", ")}catch(e){throw console.log(e),new Error(e.message)}}(e.toURI())]),"fulfilled"===t.status)&&"fulfilled"===a.status&&(i=new f(m.NAME,m.ID),n(t.value+i.getFieldValue(m.SEPARATOR.KEY)+a.value))}catch(e){Spicetify.showNotification(r.error+": "+e.message)}},e=>{var t;return 1===e.length&&(t=Spicetify.URI["Type"],Spicetify.URI.fromString(e[0]).type===t.TRACK)},"artist").register(),new Spicetify.ContextMenu.Item(r.copyImage,async e=>{var t=Spicetify.URI["Type"],a=Spicetify.URI.fromString(e[0]),i=a._base62Id||a.id;try{switch(a.type){case t.TRACK:n(await h(a.toURI()));break;case t.ALBUM:n(await y("image",a.id));break;case t.ARTIST:n(await g("image",a.toURI()));break;case t.PLAYLIST:case t.PLAYLIST_V2:n(await w("image",i));break;case t.SHOW:n(await v("image",i));break;case t.EPISODE:n(await I("image",i));break;case t.PROFILE:n(await A("image",a.username))}}catch(e){Spicetify.showNotification(r.error+": "+e.message)}},e=>{if(1!==e.length)return!1;var t=Spicetify.URI["Type"];switch(Spicetify.URI.fromString(e[0]).type){case t.ALBUM:case t.ARTIST:case t.PLAYLIST:case t.PLAYLIST_V2:case t.SHOW:case t.EPISODE:case t.PROFILE:return!0;default:return!1}},"copy").register()}}(); -------------------------------------------------------------------------------- /docs/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnthach95/spicetify-extensions/6749c9b18405df73e3b0100e221f5e29e7def234/docs/after.png -------------------------------------------------------------------------------- /docs/marketplace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnthach95/spicetify-extensions/6749c9b18405df73e3b0100e221f5e29e7def234/docs/marketplace.png -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnthach95/spicetify-extensions/6749c9b18405df73e3b0100e221f5e29e7def234/docs/settings.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Copy to Clipboard", 3 | "description": "Spicetify extension to copy text (song name, artist name, album name) to clipboard", 4 | "preview": "screenshot.png", 5 | "main": "dist/copytoclipboard.js", 6 | "authors": [ 7 | { 8 | "name": "pnthach95", 9 | "url": "https://github.com/pnthach95" 10 | }, 11 | { 12 | "name": "Tetrax-10", 13 | "url": "https://github.com/Tetrax-10" 14 | } 15 | ], 16 | "readme": "README.md" 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copytoclipboard", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "spicetify-creator", 7 | "build-local": "spicetify-creator --out=dist --minify", 8 | "watch": "spicetify-creator --watch" 9 | }, 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/react": "^18.3.3", 13 | "@types/react-dom": "^18.3.0", 14 | "prettier": "^3.3.2", 15 | "spicetify-creator": "^1.0.17" 16 | }, 17 | "dependencies": { 18 | "spcr-settings": "^1.3.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | // NAME: Copy Text 2 | // AUTHOR: pnthach95, Tetrax-10 3 | // DESCRIPTION: Adds Copy text to context menu for Spotify v1.1.59 and Spicetify v2.0.0 and above 4 | 5 | import {SettingsSection} from 'spcr-settings'; 6 | 7 | const SETTINGS = { 8 | ID: 'settings-copy-to-clipboard', 9 | NAME: 'Copy to clipboard settings', 10 | SEPARATOR: { 11 | KEY: 'ctc-separator', 12 | DESCRIPTION: 'Separator between Song name and Artist names', 13 | DEFAULT: '; ', 14 | }, 15 | }; 16 | 17 | const localizations: Record = { 18 | ru: { 19 | error: 'Ошибка', 20 | text: 'Скопировать текст', 21 | songAndArtist: 'Cкопировать трек и артиста', 22 | copied: 'Скопировано', 23 | copyImage: 'Ссылка на изображение', 24 | settings: { 25 | name: 'Copy to clipboard settings', 26 | separator: 'Separator between Song name and Artist names', 27 | }, 28 | }, 29 | en: { 30 | error: 'Error', 31 | text: 'Copy Text', 32 | songAndArtist: 'Copy Song & Artist names', 33 | copied: 'Copied', 34 | copyImage: 'Copy image link', 35 | settings: { 36 | name: SETTINGS.NAME, 37 | separator: SETTINGS.SEPARATOR.DESCRIPTION, 38 | }, 39 | }, 40 | vi: { 41 | copied: 'Đã sao chép', 42 | copyImage: 'Sao chép liên kết ảnh', 43 | error: 'Lỗi', 44 | settings: { 45 | name: 'Cài đặt Copy to clipboard', 46 | separator: 'Phân cách giữa tên bài hát và tên nghệ sĩ', 47 | }, 48 | songAndArtist: 'Sao chép tên bài hát & nghệ sĩ', 49 | text: 'Sao chép tên', 50 | }, 51 | }; 52 | 53 | async function getLocalization() { 54 | await new Promise(resolve => setTimeout(resolve, 1000)); 55 | const locale = Spicetify.Locale ? Spicetify.Locale.getLocale() : 'en'; 56 | // console.log('Spicetify.Locale._locale', Spicetify.Locale._locale); 57 | // console.log('Spicetify.Locale.getLocale', Spicetify.Locale.getLocale()); 58 | 59 | return Object.keys(localizations).includes(locale) 60 | ? localizations[locale as keyof typeof localizations] 61 | : localizations['en']; 62 | } 63 | 64 | async function fetchAlbum(dataType: DataType, id?: string) { 65 | try { 66 | const albumInfo: AlbumInfo = await Spicetify.CosmosAsync.get( 67 | `https://api.spotify.com/v1/albums/${id}`, 68 | ); 69 | if (dataType === 'image') { 70 | return albumInfo.images[0].url; 71 | } 72 | return albumInfo.name; 73 | } catch (e) { 74 | console.log(e); 75 | throw new Error((e as Error).message); 76 | } 77 | } 78 | 79 | async function fetchArtist(dataType: DataType, uri: string) { 80 | const {queryArtistOverview} = Spicetify.GraphQL.Definitions; 81 | try { 82 | const locale = Spicetify.Locale ? Spicetify.Locale.getLocale() : 'en'; 83 | const {data} = await Spicetify.GraphQL.Request(queryArtistOverview, { 84 | uri, 85 | includePrerelease: false, 86 | locale, 87 | offset: 0, 88 | limit: 10, 89 | }); 90 | if (dataType === 'image') { 91 | const img = data.artistUnion.headerImage?.data?.sources?.[0] 92 | ?.url as string; 93 | if (img) { 94 | return img; 95 | } 96 | throw new Error('No images'); 97 | } 98 | return data.artistUnion.profile.name as string; 99 | } catch (e) { 100 | console.log(e); 101 | throw new Error((e as Error).message); 102 | } 103 | } 104 | 105 | async function fetchArtists(uri: string) { 106 | const {queryTrackArtists} = Spicetify.GraphQL.Definitions; 107 | try { 108 | const {data} = (await Spicetify.GraphQL.Request(queryTrackArtists, { 109 | uri, 110 | offset: 0, 111 | limit: 10, 112 | })) as {data: QueryTrackArtistsData}; 113 | return data.trackUnion.artists.items.map(i => i.profile.name).join(', '); 114 | } catch (e) { 115 | console.log(e); 116 | throw new Error((e as Error).message); 117 | } 118 | } 119 | 120 | async function fetchTrackName(uri: string) { 121 | const {getTrackName} = Spicetify.GraphQL.Definitions; 122 | try { 123 | const {data} = (await Spicetify.GraphQL.Request(getTrackName, { 124 | uri, 125 | offset: 0, 126 | limit: 10, 127 | })) as {data: GetTrackNameData}; 128 | return data.trackUnion.name; 129 | } catch (e) { 130 | console.log(e); 131 | throw new Error((e as Error).message); 132 | } 133 | } 134 | 135 | function getLinkFromUri(uri: string) { 136 | if (uri.includes('mosaic:')) { 137 | throw new Error('Cannot copy mosaic image'); 138 | } 139 | if (uri.startsWith('spotify')) { 140 | const pictureUri = uri.split(':').pop(); 141 | if (pictureUri) { 142 | return 'https://i.scdn.co/image/' + pictureUri; 143 | } else { 144 | throw new Error('Not found'); 145 | } 146 | } else { 147 | return uri; 148 | } 149 | } 150 | 151 | async function fetchPlaylist(dataType: DataType, id: string) { 152 | try { 153 | const res = await Spicetify.CosmosAsync.get( 154 | `sp://core-playlist/v1/playlist/spotify:playlist:${id}`, 155 | ); 156 | if (dataType === 'name') { 157 | return res.playlist.name; 158 | } 159 | return getLinkFromUri(res.playlist.picture); 160 | } catch (e) { 161 | console.log(e); 162 | throw new Error((e as Error).message); 163 | } 164 | } 165 | 166 | async function fetchShow(dataType: DataType, id: string) { 167 | try { 168 | const data = await Spicetify.CosmosAsync.get( 169 | `sp://core-show/v1/shows/${id}?responseFormat=protobufJson`, 170 | ); 171 | if (dataType === 'name') { 172 | return data.header.showMetadata.name; 173 | } 174 | return getLinkFromUri(data.header.showMetadata.covers.xlargeLink); 175 | } catch (e) { 176 | console.log(e); 177 | throw new Error((e as Error).message); 178 | } 179 | } 180 | 181 | async function fetchEpisode(dataType: DataType, id: string) { 182 | try { 183 | const data = await Spicetify.Platform.ShowAPI.getEpisodeOrChapter( 184 | `spotify:episode:${id}`, 185 | ); 186 | if (dataType === 'name') { 187 | return data.name; 188 | } 189 | return getLinkFromUri( 190 | data.coverArt.reduce( 191 | (res: SpotifyImage, curr: SpotifyImage) => 192 | res.width > curr.width ? res : curr, 193 | {width: 0, height: 0, url: ''}, 194 | ).url, 195 | ); 196 | } catch (e) { 197 | console.log(e); 198 | throw new Error((e as Error).message); 199 | } 200 | } 201 | 202 | async function fetchProfile(dataType: DataType, username?: string) { 203 | try { 204 | const res = await Spicetify.CosmosAsync.get( 205 | 'sp://core-profile/v1/profiles', 206 | {usernames: username}, 207 | ); 208 | if (dataType === 'name') { 209 | return res.profiles[0].name; 210 | } 211 | return res.profiles[0].images.reduce( 212 | (res: SpotifyImageMax, curr: SpotifyImageMax) => 213 | res.maxWidth > curr.maxWidth ? res : curr, 214 | {maxWidth: 0, maxHeight: 0, url: ''}, 215 | ).url; 216 | } catch (e) { 217 | console.log(e); 218 | throw new Error((e as Error).message); 219 | } 220 | } 221 | 222 | function initCopyText(localization: Localization) { 223 | const getText: Spicetify.ContextMenu.OnClickCallback = async uris => { 224 | const {Type} = Spicetify.URI; 225 | const uri = Spicetify.URI.fromString(uris[0]); 226 | // @ts-ignore _base62Id may be existed on old versions 227 | const id: string = uri._base62Id ? uri._base62Id : uri.id; 228 | 229 | try { 230 | switch (uri.type) { 231 | case Type.TRACK: 232 | sendToClipboard(await fetchTrackName(uri.toURI())); 233 | break; 234 | case Type.LOCAL: 235 | const tmp: string[] = []; 236 | if (uri.track) { 237 | tmp.push(uri.track); 238 | } 239 | if (uri.artist) { 240 | tmp.push(uri.artist); 241 | } 242 | if (uri.album) { 243 | tmp.push(uri.album); 244 | } 245 | sendToClipboard(tmp.join('; ')); 246 | break; 247 | case Type.LOCAL_ARTIST: 248 | sendToClipboard(`${uri.artist ? uri.artist : ''}`); 249 | break; 250 | case Type.LOCAL_ALBUM: 251 | sendToClipboard(`${uri.album ? uri.album : ''}`); 252 | break; 253 | case Type.ALBUM: 254 | sendToClipboard(await fetchAlbum('name', uri.id)); 255 | break; 256 | case Type.ARTIST: 257 | sendToClipboard(await fetchArtist('name', uri.toURI())); 258 | break; 259 | case Type.PLAYLIST: 260 | case Type.PLAYLIST_V2: 261 | sendToClipboard(await fetchPlaylist('name', id)); 262 | break; 263 | case Type.SHOW: 264 | sendToClipboard(await fetchShow('name', id)); 265 | break; 266 | case Type.EPISODE: 267 | sendToClipboard(await fetchEpisode('name', id)); 268 | break; 269 | case Type.PROFILE: 270 | sendToClipboard(await fetchProfile('name', uri.username)); 271 | break; 272 | case Type.FOLDER: 273 | let rootlist: RootlistContent = 274 | await Spicetify.Platform.RootlistAPI.getContents(); 275 | let folder = rootlist.items.filter( 276 | item => item.type === 'folder' && item.uri.includes(id), 277 | ); 278 | sendToClipboard(folder[0].name); 279 | break; 280 | default: 281 | break; 282 | } 283 | } catch (error) { 284 | Spicetify.showNotification( 285 | `${localization.error}: ${(error as Error).message}`, 286 | ); 287 | } 288 | }; 289 | 290 | const getImage: Spicetify.ContextMenu.OnClickCallback = async uris => { 291 | const {Type} = Spicetify.URI; 292 | const uri = Spicetify.URI.fromString(uris[0]); 293 | // @ts-ignore _base62Id may be existed on old versions 294 | const id: string = uri._base62Id ? uri._base62Id : uri.id; 295 | 296 | try { 297 | switch (uri.type) { 298 | case Type.TRACK: 299 | sendToClipboard(await fetchTrackName(uri.toURI())); 300 | break; 301 | case Type.ALBUM: 302 | sendToClipboard(await fetchAlbum('image', uri.id)); 303 | break; 304 | case Type.ARTIST: 305 | sendToClipboard(await fetchArtist('image', uri.toURI())); 306 | break; 307 | case Type.PLAYLIST: 308 | case Type.PLAYLIST_V2: 309 | sendToClipboard(await fetchPlaylist('image', id)); 310 | break; 311 | case Type.SHOW: 312 | sendToClipboard(await fetchShow('image', id)); 313 | break; 314 | case Type.EPISODE: 315 | sendToClipboard(await fetchEpisode('image', id)); 316 | break; 317 | case Type.PROFILE: 318 | sendToClipboard(await fetchProfile('image', uri.username)); 319 | break; 320 | default: 321 | break; 322 | } 323 | } catch (error) { 324 | Spicetify.showNotification( 325 | `${localization.error}: ${(error as Error).message}`, 326 | ); 327 | } 328 | }; 329 | 330 | const getSongArtistText: Spicetify.ContextMenu.OnClickCallback = 331 | async uris => { 332 | const {Type} = Spicetify.URI; 333 | const uri = Spicetify.URI.fromString(uris[0]); 334 | 335 | try { 336 | switch (uri.type) { 337 | case Type.TRACK: 338 | const [name, artists] = await Promise.allSettled([ 339 | fetchTrackName(uri.toURI()), 340 | fetchArtists(uri.toURI()), 341 | ]); 342 | if (name.status === 'fulfilled' && artists.status === 'fulfilled') { 343 | const settings = new SettingsSection(SETTINGS.NAME, SETTINGS.ID); 344 | sendToClipboard( 345 | name.value + 346 | settings.getFieldValue(SETTINGS.SEPARATOR.KEY) + 347 | artists.value, 348 | ); 349 | } 350 | break; 351 | default: 352 | break; 353 | } 354 | } catch (error) { 355 | Spicetify.showNotification( 356 | `${localization.error}: ${(error as Error).message}`, 357 | ); 358 | } 359 | }; 360 | 361 | function sendToClipboard(text: string | null) { 362 | if (text) { 363 | Spicetify.showNotification(`${localization.copied}: ${text}`); 364 | Spicetify.Platform.ClipboardAPI.copy(text); 365 | } 366 | } 367 | 368 | const shouldAddContextMenu: Spicetify.ContextMenu.ShouldAddCallback = 369 | uris => { 370 | if (uris.length === 1) { 371 | const {Type} = Spicetify.URI; 372 | const uri = Spicetify.URI.fromString(uris[0]); 373 | switch (uri.type) { 374 | case Type.TRACK: 375 | case Type.LOCAL: 376 | case Type.LOCAL_ARTIST: 377 | case Type.LOCAL_ALBUM: 378 | case Type.ALBUM: 379 | case Type.ARTIST: 380 | case Type.PLAYLIST: 381 | case Type.PLAYLIST_V2: 382 | case Type.SHOW: 383 | case Type.EPISODE: 384 | case Type.PROFILE: 385 | case Type.FOLDER: 386 | return true; 387 | default: 388 | return false; 389 | } 390 | } else { 391 | return false; 392 | } 393 | }; 394 | 395 | const shouldAddCSAContextMenu: Spicetify.ContextMenu.ShouldAddCallback = 396 | uris => { 397 | if (uris.length === 1) { 398 | const {Type} = Spicetify.URI; 399 | const uri = Spicetify.URI.fromString(uris[0]); 400 | switch (uri.type) { 401 | case Type.TRACK: 402 | return true; 403 | default: 404 | return false; 405 | } 406 | } else { 407 | return false; 408 | } 409 | }; 410 | 411 | const shouldAddCopyImageContextMenu: Spicetify.ContextMenu.ShouldAddCallback = 412 | uris => { 413 | if (uris.length === 1) { 414 | const {Type} = Spicetify.URI; 415 | const uri = Spicetify.URI.fromString(uris[0]); 416 | switch (uri.type) { 417 | case Type.ALBUM: 418 | case Type.ARTIST: 419 | case Type.PLAYLIST: 420 | case Type.PLAYLIST_V2: 421 | case Type.SHOW: 422 | case Type.EPISODE: 423 | case Type.PROFILE: 424 | return true; 425 | default: 426 | return false; 427 | } 428 | } else { 429 | return false; 430 | } 431 | }; 432 | 433 | new Spicetify.ContextMenu.Item( 434 | localization.text, 435 | getText, 436 | shouldAddContextMenu, 437 | 'copy', 438 | ).register(); 439 | new Spicetify.ContextMenu.Item( 440 | localization.songAndArtist, 441 | getSongArtistText, 442 | shouldAddCSAContextMenu, 443 | 'artist', 444 | ).register(); 445 | new Spicetify.ContextMenu.Item( 446 | localization.copyImage, 447 | getImage, 448 | shouldAddCopyImageContextMenu, 449 | 'copy', 450 | ).register(); 451 | } 452 | 453 | async function main() { 454 | while (!Spicetify || document.readyState !== 'complete') { 455 | await new Promise(resolve => setTimeout(resolve, 1000)); 456 | } 457 | 458 | const localization = await getLocalization(); 459 | const settings = new SettingsSection(localization.settings.name, SETTINGS.ID); 460 | settings.addInput( 461 | SETTINGS.SEPARATOR.KEY, 462 | localization.settings.separator, 463 | SETTINGS.SEPARATOR.DEFAULT, 464 | ); 465 | settings.pushSettings(); 466 | 467 | initCopyText(localization); 468 | } 469 | 470 | export default main; 471 | -------------------------------------------------------------------------------- /src/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "nameId": "copytoclipboard" 3 | } -------------------------------------------------------------------------------- /src/types/css-modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' { 2 | const classes: {[key: string]: string}; 3 | export default classes; 4 | } 5 | 6 | declare module '*.module.scss' { 7 | const classes: {[key: string]: string}; 8 | export default classes; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/spicetify.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Spicetify { 2 | type Icon = 3 | | 'album' 4 | | 'artist' 5 | | 'block' 6 | | 'brightness' 7 | | 'car' 8 | | 'chart-down' 9 | | 'chart-up' 10 | | 'check' 11 | | 'check-alt-fill' 12 | | 'chevron-left' 13 | | 'chevron-right' 14 | | 'chromecast-disconnected' 15 | | 'clock' 16 | | 'collaborative' 17 | | 'computer' 18 | | 'copy' 19 | | 'download' 20 | | 'downloaded' 21 | | 'edit' 22 | | 'enhance' 23 | | 'exclamation-circle' 24 | | 'external-link' 25 | | 'facebook' 26 | | 'follow' 27 | | 'fullscreen' 28 | | 'gamepad' 29 | | 'grid-view' 30 | | 'heart' 31 | | 'heart-active' 32 | | 'instagram' 33 | | 'laptop' 34 | | 'library' 35 | | 'list-view' 36 | | 'location' 37 | | 'locked' 38 | | 'locked-active' 39 | | 'lyrics' 40 | | 'menu' 41 | | 'minimize' 42 | | 'minus' 43 | | 'more' 44 | | 'new-spotify-connect' 45 | | 'offline' 46 | | 'pause' 47 | | 'phone' 48 | | 'play' 49 | | 'playlist' 50 | | 'playlist-folder' 51 | | 'plus-alt' 52 | | 'plus2px' 53 | | 'podcasts' 54 | | 'projector' 55 | | 'queue' 56 | | 'repeat' 57 | | 'repeat-once' 58 | | 'search' 59 | | 'search-active' 60 | | 'shuffle' 61 | | 'skip-back' 62 | | 'skip-back15' 63 | | 'skip-forward' 64 | | 'skip-forward15' 65 | | 'soundbetter' 66 | | 'speaker' 67 | | 'spotify' 68 | | 'subtitles' 69 | | 'tablet' 70 | | 'ticket' 71 | | 'twitter' 72 | | 'visualizer' 73 | | 'voice' 74 | | 'volume' 75 | | 'volume-off' 76 | | 'volume-one-wave' 77 | | 'volume-two-wave' 78 | | 'watch' 79 | | 'x'; 80 | type Variant = 81 | | 'bass' 82 | | 'forte' 83 | | 'brio' 84 | | 'altoBrio' 85 | | 'alto' 86 | | 'canon' 87 | | 'celloCanon' 88 | | 'cello' 89 | | 'ballad' 90 | | 'balladBold' 91 | | 'viola' 92 | | 'violaBold' 93 | | 'mesto' 94 | | 'mestoBold' 95 | | 'metronome' 96 | | 'finale' 97 | | 'finaleBold' 98 | | 'minuet' 99 | | 'minuetBold'; 100 | type SemanticColor = 101 | | 'textBase' 102 | | 'textSubdued' 103 | | 'textBrightAccent' 104 | | 'textNegative' 105 | | 'textWarning' 106 | | 'textPositive' 107 | | 'textAnnouncement' 108 | | 'essentialBase' 109 | | 'essentialSubdued' 110 | | 'essentialBrightAccent' 111 | | 'essentialNegative' 112 | | 'essentialWarning' 113 | | 'essentialPositive' 114 | | 'essentialAnnouncement' 115 | | 'decorativeBase' 116 | | 'decorativeSubdued' 117 | | 'backgroundBase' 118 | | 'backgroundHighlight' 119 | | 'backgroundPress' 120 | | 'backgroundElevatedBase' 121 | | 'backgroundElevatedHighlight' 122 | | 'backgroundElevatedPress' 123 | | 'backgroundTintedBase' 124 | | 'backgroundTintedHighlight' 125 | | 'backgroundTintedPress' 126 | | 'backgroundUnsafeForSmallTextBase' 127 | | 'backgroundUnsafeForSmallTextHighlight' 128 | | 'backgroundUnsafeForSmallTextPress'; 129 | type ColorSet = 130 | | 'base' 131 | | 'brightAccent' 132 | | 'negative' 133 | | 'warning' 134 | | 'positive' 135 | | 'announcement' 136 | | 'invertedDark' 137 | | 'invertedLight' 138 | | 'mutedAccent' 139 | | 'overMedia'; 140 | type ColorSetBackgroundColors = { 141 | base: string; 142 | highlight: string; 143 | press: string; 144 | }; 145 | type ColorSetNamespaceColors = { 146 | announcement: string; 147 | base: string; 148 | brightAccent: string; 149 | negative: string; 150 | positive: string; 151 | subdued: string; 152 | warning: string; 153 | }; 154 | type ColorSetBody = { 155 | background: ColorSetBackgroundColors & { 156 | elevated: ColorSetBackgroundColors; 157 | tinted: ColorSetBackgroundColors; 158 | unsafeForSmallText: ColorSetBackgroundColors; 159 | }; 160 | decorative: { 161 | base: string; 162 | subdued: string; 163 | }; 164 | essential: ColorSetNamespaceColors; 165 | text: ColorSetNamespaceColors; 166 | }; 167 | type Metadata = Partial>; 168 | type ContextTrack = { 169 | uri: string; 170 | uid?: string; 171 | metadata?: Metadata; 172 | }; 173 | type PlayerState = { 174 | timestamp: number; 175 | context: PlayerContext; 176 | index: PlayerIndex; 177 | item: PlayerTrack; 178 | shuffle: boolean; 179 | repeat: number; 180 | speed: number; 181 | positionAsOfTimestamp: number; 182 | duration: number; 183 | hasContext: boolean; 184 | isPaused: boolean; 185 | isBuffering: boolean; 186 | restrictions: Restrictions; 187 | previousItems?: PlayerTrack[]; 188 | nextItems?: PlayerTrack[]; 189 | playbackQuality: PlaybackQuality; 190 | playbackId: string; 191 | sessionId: string; 192 | signals?: any[]; 193 | }; 194 | type PlayerContext = { 195 | uri: string; 196 | url: string; 197 | metadata: { 198 | 'player.arch': string; 199 | }; 200 | }; 201 | type PlayerIndex = { 202 | pageURI?: string | null; 203 | pageIndex: number; 204 | itemIndex: number; 205 | }; 206 | type PlayerTrack = { 207 | type: string; 208 | uri: string; 209 | uid: string; 210 | name: string; 211 | mediaType: string; 212 | duration: { 213 | milliseconds: number; 214 | }; 215 | album: Album; 216 | artists?: ArtistsEntity[]; 217 | isLocal: boolean; 218 | isExplicit: boolean; 219 | is19PlusOnly: boolean; 220 | provider: string; 221 | metadata: TrackMetadata; 222 | images?: ImagesEntity[]; 223 | }; 224 | type TrackMetadata = { 225 | artist_uri: string; 226 | entity_uri: string; 227 | iteration: string; 228 | title: string; 229 | 'collection.is_banned': string; 230 | 'artist_uri:1': string; 231 | 'collection.in_collection': string; 232 | image_small_url: string; 233 | 'collection.can_ban': string; 234 | is_explicit: string; 235 | album_disc_number: string; 236 | album_disc_count: string; 237 | track_player: string; 238 | album_title: string; 239 | 'collection.can_add': string; 240 | image_large_url: string; 241 | 'actions.skipping_prev_past_track': string; 242 | page_instance_id: string; 243 | image_xlarge_url: string; 244 | marked_for_download: string; 245 | 'actions.skipping_next_past_track': string; 246 | context_uri: string; 247 | 'artist_name:1': string; 248 | has_lyrics: string; 249 | interaction_id: string; 250 | image_url: string; 251 | album_uri: string; 252 | album_artist_name: string; 253 | album_track_number: string; 254 | artist_name: string; 255 | duration: string; 256 | album_track_count: string; 257 | popularity: string; 258 | }; 259 | type Album = { 260 | type: string; 261 | uri: string; 262 | name: string; 263 | images?: ImagesEntity[]; 264 | }; 265 | type ImagesEntity = { 266 | url: string; 267 | label: string; 268 | }; 269 | type ArtistsEntity = { 270 | type: string; 271 | uri: string; 272 | name: string; 273 | }; 274 | type Restrictions = { 275 | canPause: boolean; 276 | canResume: boolean; 277 | canSeek: boolean; 278 | canSkipPrevious: boolean; 279 | canSkipNext: boolean; 280 | canToggleRepeatContext: boolean; 281 | canToggleRepeatTrack: boolean; 282 | canToggleShuffle: boolean; 283 | disallowPausingReasons?: string[]; 284 | disallowResumingReasons?: string[]; 285 | disallowSeekingReasons?: string[]; 286 | disallowSkippingPreviousReasons?: string[]; 287 | disallowSkippingNextReasons?: string[]; 288 | disallowTogglingRepeatContextReasons?: string[]; 289 | disallowTogglingRepeatTrackReasons?: string[]; 290 | disallowTogglingShuffleReasons?: string[]; 291 | disallowTransferringPlaybackReasons?: string[]; 292 | }; 293 | type PlaybackQuality = { 294 | bitrateLevel: number; 295 | strategy: number; 296 | targetBitrateLevel: number; 297 | targetBitrateAvailable: boolean; 298 | hifiStatus: number; 299 | }; 300 | namespace Player { 301 | /** 302 | * Register a listener `type` on Spicetify.Player. 303 | * 304 | * On default, `Spicetify.Player` always dispatch: 305 | * - `songchange` type when player changes track. 306 | * - `onplaypause` type when player plays or pauses. 307 | * - `onprogress` type when track progress changes. 308 | * - `appchange` type when user changes page. 309 | */ 310 | function addEventListener( 311 | type: string, 312 | callback: (event?: Event) => void, 313 | ): void; 314 | function addEventListener( 315 | type: 'songchange', 316 | callback: (event?: Event & {data: PlayerState}) => void, 317 | ): void; 318 | function addEventListener( 319 | type: 'onplaypause', 320 | callback: (event?: Event & {data: PlayerState}) => void, 321 | ): void; 322 | function addEventListener( 323 | type: 'onprogress', 324 | callback: (event?: Event & {data: number}) => void, 325 | ): void; 326 | function addEventListener( 327 | type: 'appchange', 328 | callback: ( 329 | event?: Event & { 330 | data: { 331 | /** 332 | * App href path 333 | */ 334 | path: string; 335 | /** 336 | * App container 337 | */ 338 | container: HTMLElement; 339 | }; 340 | }, 341 | ) => void, 342 | ): void; 343 | /** 344 | * Skip to previous track. 345 | */ 346 | function back(): void; 347 | /** 348 | * An object contains all information about current track and player. 349 | */ 350 | const data: PlayerState; 351 | /** 352 | * Decrease a small amount of volume. 353 | */ 354 | function decreaseVolume(): void; 355 | /** 356 | * Dispatches an event at `Spicetify.Player`. 357 | * 358 | * On default, `Spicetify.Player` always dispatch 359 | * - `songchange` type when player changes track. 360 | * - `onplaypause` type when player plays or pauses. 361 | * - `onprogress` type when track progress changes. 362 | * - `appchange` type when user changes page. 363 | */ 364 | function dispatchEvent(event: Event): void; 365 | const eventListeners: { 366 | [key: string]: Array<(event?: Event) => void>; 367 | }; 368 | /** 369 | * Convert milisecond to `mm:ss` format 370 | * @param milisecond 371 | */ 372 | function formatTime(milisecond: number): string; 373 | /** 374 | * Return song total duration in milisecond. 375 | */ 376 | function getDuration(): number; 377 | /** 378 | * Return mute state 379 | */ 380 | function getMute(): boolean; 381 | /** 382 | * Return elapsed duration in milisecond. 383 | */ 384 | function getProgress(): number; 385 | /** 386 | * Return elapsed duration in percentage (0 to 1). 387 | */ 388 | function getProgressPercent(): number; 389 | /** 390 | * Return current Repeat state (No repeat = 0/Repeat all = 1/Repeat one = 2). 391 | */ 392 | function getRepeat(): number; 393 | /** 394 | * Return current shuffle state. 395 | */ 396 | function getShuffle(): boolean; 397 | /** 398 | * Return track heart state. 399 | */ 400 | function getHeart(): boolean; 401 | /** 402 | * Return current volume level (0 to 1). 403 | */ 404 | function getVolume(): number; 405 | /** 406 | * Increase a small amount of volume. 407 | */ 408 | function increaseVolume(): void; 409 | /** 410 | * Return a boolean whether player is playing. 411 | */ 412 | function isPlaying(): boolean; 413 | /** 414 | * Skip to next track. 415 | */ 416 | function next(): void; 417 | /** 418 | * Pause track. 419 | */ 420 | function pause(): void; 421 | /** 422 | * Resume track. 423 | */ 424 | function play(): void; 425 | /** 426 | * Play a track, playlist, album, etc. immediately 427 | * @param uri Spotify URI 428 | * @param context 429 | * @param options 430 | */ 431 | function playUri(uri: string, context?: any, options?: any): Promise; 432 | /** 433 | * Unregister added event listener `type`. 434 | * @param type 435 | * @param callback 436 | */ 437 | function removeEventListener( 438 | type: string, 439 | callback: (event?: Event) => void, 440 | ): void; 441 | /** 442 | * Seek track to position. 443 | * @param position can be in percentage (0 to 1) or in milisecond. 444 | */ 445 | function seek(position: number): void; 446 | /** 447 | * Turn mute on/off 448 | * @param state 449 | */ 450 | function setMute(state: boolean): void; 451 | /** 452 | * Change Repeat mode 453 | * @param mode `0` No repeat. `1` Repeat all. `2` Repeat one track. 454 | */ 455 | function setRepeat(mode: number): void; 456 | /** 457 | * Turn shuffle on/off. 458 | * @param state 459 | */ 460 | function setShuffle(state: boolean): void; 461 | /** 462 | * Set volume level 463 | * @param level 0 to 1 464 | */ 465 | function setVolume(level: number): void; 466 | /** 467 | * Seek to previous `amount` of milisecond 468 | * @param amount in milisecond. Default: 15000. 469 | */ 470 | function skipBack(amount?: number): void; 471 | /** 472 | * Seek to next `amount` of milisecond 473 | * @param amount in milisecond. Default: 15000. 474 | */ 475 | function skipForward(amount?: number): void; 476 | /** 477 | * Toggle Heart (Favourite) track state. 478 | */ 479 | function toggleHeart(): void; 480 | /** 481 | * Toggle Mute/No mute. 482 | */ 483 | function toggleMute(): void; 484 | /** 485 | * Toggle Play/Pause. 486 | */ 487 | function togglePlay(): void; 488 | /** 489 | * Toggle No repeat/Repeat all/Repeat one. 490 | */ 491 | function toggleRepeat(): void; 492 | /** 493 | * Toggle Shuffle/No shuffle. 494 | */ 495 | function toggleShuffle(): void; 496 | } 497 | /** 498 | * Adds a track or array of tracks to prioritized queue. 499 | */ 500 | function addToQueue(uri: ContextTrack[]): Promise; 501 | /** 502 | * @deprecated 503 | */ 504 | const BridgeAPI: any; 505 | /** 506 | * @deprecated 507 | */ 508 | const CosmosAPI: any; 509 | /** 510 | * Async wrappers of CosmosAPI 511 | */ 512 | namespace CosmosAsync { 513 | type Method = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'SUB'; 514 | interface Error { 515 | code: number; 516 | error: string; 517 | message: string; 518 | stack?: string; 519 | } 520 | 521 | type Headers = Record; 522 | type Body = Record; 523 | 524 | interface Response { 525 | body: any; 526 | headers: Headers; 527 | status: number; 528 | uri?: string; 529 | } 530 | 531 | function head(url: string, headers?: Headers): Promise; 532 | function get( 533 | url: string, 534 | body?: Body, 535 | headers?: Headers, 536 | ): Promise; 537 | function post( 538 | url: string, 539 | body?: Body, 540 | headers?: Headers, 541 | ): Promise; 542 | function put( 543 | url: string, 544 | body?: Body, 545 | headers?: Headers, 546 | ): Promise; 547 | function del( 548 | url: string, 549 | body?: Body, 550 | headers?: Headers, 551 | ): Promise; 552 | function patch( 553 | url: string, 554 | body?: Body, 555 | headers?: Headers, 556 | ): Promise; 557 | function sub( 558 | url: string, 559 | callback: (b: Response['body']) => void, 560 | onError?: (e: Error) => void, 561 | body?: Body, 562 | headers?: Headers, 563 | ): Promise; 564 | function postSub( 565 | url: string, 566 | body: Body | null, 567 | callback: (b: Response['body']) => void, 568 | onError?: (e: Error) => void, 569 | ): Promise; 570 | function request( 571 | method: Method, 572 | url: string, 573 | body?: Body, 574 | headers?: Headers, 575 | ): Promise; 576 | function resolve( 577 | method: Method, 578 | url: string, 579 | body?: Body, 580 | headers?: Headers, 581 | ): Promise; 582 | } 583 | /** 584 | * Fetch interesting colors from URI. 585 | * @param uri Any type of URI that has artwork (playlist, track, album, artist, show, ...) 586 | */ 587 | function colorExtractor(uri: string): Promise<{ 588 | DARK_VIBRANT: string; 589 | DESATURATED: string; 590 | LIGHT_VIBRANT: string; 591 | PROMINENT: string; 592 | VIBRANT: string; 593 | VIBRANT_NON_ALARMING: string; 594 | }>; 595 | /** 596 | * @deprecated 597 | */ 598 | function getAblumArtColors(): any; 599 | /** 600 | * Fetch track analyzed audio data. 601 | * Beware, not all tracks have audio data. 602 | * @param uri is optional. Leave it blank to get current track 603 | * or specify another track uri. 604 | */ 605 | function getAudioData(uri?: string): Promise; 606 | /** 607 | * Set of APIs method to register, deregister hotkeys/shortcuts 608 | */ 609 | namespace Keyboard { 610 | type ValidKey = 611 | | 'BACKSPACE' 612 | | 'TAB' 613 | | 'ENTER' 614 | | 'SHIFT' 615 | | 'CTRL' 616 | | 'ALT' 617 | | 'CAPS' 618 | | 'ESCAPE' 619 | | 'SPACE' 620 | | 'PAGE_UP' 621 | | 'PAGE_DOWN' 622 | | 'END' 623 | | 'HOME' 624 | | 'ARROW_LEFT' 625 | | 'ARROW_UP' 626 | | 'ARROW_RIGHT' 627 | | 'ARROW_DOWN' 628 | | 'INSERT' 629 | | 'DELETE' 630 | | 'A' 631 | | 'B' 632 | | 'C' 633 | | 'D' 634 | | 'E' 635 | | 'F' 636 | | 'G' 637 | | 'H' 638 | | 'I' 639 | | 'J' 640 | | 'K' 641 | | 'L' 642 | | 'M' 643 | | 'N' 644 | | 'O' 645 | | 'P' 646 | | 'Q' 647 | | 'R' 648 | | 'S' 649 | | 'T' 650 | | 'U' 651 | | 'V' 652 | | 'W' 653 | | 'X' 654 | | 'Y' 655 | | 'Z' 656 | | 'WINDOW_LEFT' 657 | | 'WINDOW_RIGHT' 658 | | 'SELECT' 659 | | 'NUMPAD_0' 660 | | 'NUMPAD_1' 661 | | 'NUMPAD_2' 662 | | 'NUMPAD_3' 663 | | 'NUMPAD_4' 664 | | 'NUMPAD_5' 665 | | 'NUMPAD_6' 666 | | 'NUMPAD_7' 667 | | 'NUMPAD_8' 668 | | 'NUMPAD_9' 669 | | 'MULTIPLY' 670 | | 'ADD' 671 | | 'SUBTRACT' 672 | | 'DECIMAL_POINT' 673 | | 'DIVIDE' 674 | | 'F1' 675 | | 'F2' 676 | | 'F3' 677 | | 'F4' 678 | | 'F5' 679 | | 'F6' 680 | | 'F7' 681 | | 'F8' 682 | | 'F9' 683 | | 'F10' 684 | | 'F11' 685 | | 'F12' 686 | | ';' 687 | | '=' 688 | | ' | ' 689 | | '-' 690 | | '.' 691 | | '/' 692 | | '`' 693 | | '[' 694 | | '\\' 695 | | ']' 696 | | '"' 697 | | '~' 698 | | '!' 699 | | '@' 700 | | '#' 701 | | '$' 702 | | '%' 703 | | '^' 704 | | '&' 705 | | '*' 706 | | '(' 707 | | ')' 708 | | '_' 709 | | '+' 710 | | ':' 711 | | '<' 712 | | '>' 713 | | '?' 714 | | '|'; 715 | type KeysDefine = 716 | | string 717 | | { 718 | key: string; 719 | ctrl?: boolean; 720 | shift?: boolean; 721 | alt?: boolean; 722 | meta?: boolean; 723 | }; 724 | const KEYS: Record; 725 | function registerShortcut( 726 | keys: KeysDefine, 727 | callback: (event: KeyboardEvent) => void, 728 | ): void; 729 | function registerIsolatedShortcut( 730 | keys: KeysDefine, 731 | callback: (event: KeyboardEvent) => void, 732 | ): void; 733 | function registerImportantShortcut( 734 | keys: KeysDefine, 735 | callback: (event: KeyboardEvent) => void, 736 | ): void; 737 | function _deregisterShortcut(keys: KeysDefine): void; 738 | function deregisterImportantShortcut(keys: KeysDefine): void; 739 | function changeShortcut(keys: KeysDefine, newKeys: KeysDefine): void; 740 | } 741 | 742 | /** 743 | * @deprecated 744 | */ 745 | const LiveAPI: any; 746 | 747 | namespace LocalStorage { 748 | /** 749 | * Empties the list associated with the object of all key/value pairs, if there are any. 750 | */ 751 | function clear(): void; 752 | /** 753 | * Get key value 754 | */ 755 | function get(key: string): string | null; 756 | /** 757 | * Delete key 758 | */ 759 | function remove(key: string): void; 760 | /** 761 | * Set new value for key 762 | */ 763 | function set(key: string, value: string): void; 764 | } 765 | /** 766 | * To create and prepend custom menu item in profile menu. 767 | */ 768 | namespace Menu { 769 | /** 770 | * Create a single toggle. 771 | */ 772 | class Item { 773 | constructor( 774 | name: string, 775 | isEnabled: boolean, 776 | onClick: (self: Item) => void, 777 | icon?: Icon | string, 778 | ); 779 | name: string; 780 | isEnabled: boolean; 781 | /** 782 | * Change item name 783 | */ 784 | setName(name: string): void; 785 | /** 786 | * Change item enabled state. 787 | * Visually, item would has a tick next to it if its state is enabled. 788 | */ 789 | setState(isEnabled: boolean): void; 790 | /** 791 | * Change icon 792 | */ 793 | setIcon(icon: Icon | string): void; 794 | /** 795 | * Item is only available in Profile menu when method "register" is called. 796 | */ 797 | register(): void; 798 | /** 799 | * Stop item to be prepended into Profile menu. 800 | */ 801 | deregister(): void; 802 | } 803 | 804 | /** 805 | * Create a sub menu to contain Item toggles. 806 | * `Item`s in `subItems` array shouldn't be registered. 807 | */ 808 | class SubMenu { 809 | constructor(name: string, subItems: Item[]); 810 | name: string; 811 | /** 812 | * Change SubMenu name 813 | */ 814 | setName(name: string): void; 815 | /** 816 | * Add an item to sub items list 817 | */ 818 | addItem(item: Item); 819 | /** 820 | * Remove an item from sub items list 821 | */ 822 | removeItem(item: Item); 823 | /** 824 | * SubMenu is only available in Profile menu when method "register" is called. 825 | */ 826 | register(): void; 827 | /** 828 | * Stop SubMenu to be prepended into Profile menu. 829 | */ 830 | deregister(): void; 831 | } 832 | } 833 | 834 | /** 835 | * Keyboard shortcut library 836 | * 837 | * Documentation: https://craig.is/killing/mice v1.6.5 838 | * 839 | * Spicetify.Keyboard is wrapper of this library to be compatible with legacy Spotify, 840 | * so new extension should use this library instead. 841 | */ 842 | function Mousetrap(element?: any): void; 843 | 844 | /** 845 | * Contains vast array of internal APIs. 846 | * Please explore in Devtool Console. 847 | */ 848 | const Platform: any; 849 | /** 850 | * Queue object contains list of queuing tracks, 851 | * history of played tracks and current track metadata. 852 | */ 853 | const Queue: { 854 | nextTracks: any[]; 855 | prevTracks: any[]; 856 | queueRevision: string; 857 | track: any; 858 | }; 859 | /** 860 | * Remove a track or array of tracks from current queue. 861 | */ 862 | function removeFromQueue(uri: ContextTrack[]): Promise; 863 | /** 864 | * Display a bubble of notification. Useful for a visual feedback. 865 | * @param message Message to display. Can use inline HTML for styling. 866 | * @param isError If true, bubble will be red. Defaults to false. 867 | * @param msTimeout Time in milliseconds to display the bubble. Defaults to Spotify's value. 868 | */ 869 | function showNotification( 870 | message: React.ReactNode, 871 | isError?: boolean, 872 | msTimeout?: number, 873 | ): void; 874 | /** 875 | * Set of APIs method to parse and validate URIs. 876 | */ 877 | class URI { 878 | constructor(type: string, props: any); 879 | public type: string; 880 | public hasBase62Id: boolean; 881 | 882 | public id?: string; 883 | public disc?: any; 884 | public args?: any; 885 | public category?: string; 886 | public username?: string; 887 | public track?: string; 888 | public artist?: string; 889 | public album?: string; 890 | public duration?: number; 891 | public query?: string; 892 | public country?: string; 893 | public global?: boolean; 894 | public context?: string | typeof URI | null; 895 | public anchor?: string; 896 | public play?: any; 897 | public toplist?: any; 898 | 899 | /** 900 | * 901 | * @return The URI representation of this uri. 902 | */ 903 | toURI(): string; 904 | 905 | /** 906 | * 907 | * @return The URI representation of this uri. 908 | */ 909 | toString(): string; 910 | 911 | /** 912 | * Get the URL path of this uri. 913 | * 914 | * @param opt_leadingSlash True if a leading slash should be prepended. 915 | * @return The path of this uri. 916 | */ 917 | toURLPath(opt_leadingSlash: boolean): string; 918 | 919 | /** 920 | * 921 | * @param origin The origin to use for the URL. 922 | * @return The URL string for the uri. 923 | */ 924 | toURL(origin?: string): string; 925 | 926 | /** 927 | * Clones a given SpotifyURI instance. 928 | * 929 | * @return An instance of URI. 930 | */ 931 | clone(): URI | null; 932 | 933 | /** 934 | * Gets the path of the URI object by removing all hash and query parameters. 935 | * 936 | * @return The path of the URI object. 937 | */ 938 | getPath(): string; 939 | 940 | /** 941 | * The various URI Types. 942 | * 943 | * Note that some of the types in this enum are not real URI types, but are 944 | * actually URI particles. They are marked so. 945 | * 946 | */ 947 | static Type: { 948 | AD: string; 949 | ALBUM: string; 950 | GENRE: string; 951 | QUEUE: string; 952 | APPLICATION: string; 953 | ARTIST: string; 954 | ARTIST_TOPLIST: string; 955 | ARTIST_CONCERTS: string; 956 | AUDIO_FILE: string; 957 | COLLECTION: string; 958 | COLLECTION_ALBUM: string; 959 | COLLECTION_ARTIST: string; 960 | COLLECTION_MISSING_ALBUM: string; 961 | COLLECTION_TRACK_LIST: string; 962 | CONCERT: string; 963 | CONTEXT_GROUP: string; 964 | DAILY_MIX: string; 965 | EMPTY: string; 966 | EPISODE: string; 967 | /** URI particle; not an actual URI. */ 968 | FACEBOOK: string; 969 | FOLDER: string; 970 | FOLLOWERS: string; 971 | FOLLOWING: string; 972 | IMAGE: string; 973 | INBOX: string; 974 | INTERRUPTION: string; 975 | LIBRARY: string; 976 | LIVE: string; 977 | ROOM: string; 978 | EXPRESSION: string; 979 | LOCAL: string; 980 | LOCAL_TRACK: string; 981 | LOCAL_ALBUM: string; 982 | LOCAL_ARTIST: string; 983 | MERCH: string; 984 | MOSAIC: string; 985 | PLAYLIST: string; 986 | PLAYLIST_V2: string; 987 | PRERELEASE: string; 988 | PROFILE: string; 989 | PUBLISHED_ROOTLIST: string; 990 | RADIO: string; 991 | ROOTLIST: string; 992 | SEARCH: string; 993 | SHOW: string; 994 | SOCIAL_SESSION: string; 995 | SPECIAL: string; 996 | STARRED: string; 997 | STATION: string; 998 | TEMP_PLAYLIST: string; 999 | TOPLIST: string; 1000 | TRACK: string; 1001 | TRACKSET: string; 1002 | USER_TOPLIST: string; 1003 | USER_TOP_TRACKS: string; 1004 | UNKNOWN: string; 1005 | MEDIA: string; 1006 | QUESTION: string; 1007 | POLL: string; 1008 | }; 1009 | 1010 | /** 1011 | * Creates a new URI object from a parsed string argument. 1012 | * 1013 | * @param str The string that will be parsed into a URI object. 1014 | * @throws TypeError If the string argument is not a valid URI, a TypeError will 1015 | * be thrown. 1016 | * @return The parsed URI object. 1017 | */ 1018 | static fromString(str: string): URI; 1019 | 1020 | /** 1021 | * Parses a given object into a URI instance. 1022 | * 1023 | * Unlike URI.fromString, this function could receive any kind of value. If 1024 | * the value is already a URI instance, it is simply returned. 1025 | * Otherwise the value will be stringified before parsing. 1026 | * 1027 | * This function also does not throw an error like URI.fromString, but 1028 | * instead simply returns null if it can't parse the value. 1029 | * 1030 | * @param value The value to parse. 1031 | * @return The corresponding URI instance, or null if the 1032 | * passed value is not a valid value. 1033 | */ 1034 | static from(value: any): URI | null; 1035 | 1036 | /** 1037 | * Checks whether two URI:s refer to the same thing even though they might 1038 | * not necessarily be equal. 1039 | * 1040 | * These two Playlist URIs, for example, refer to the same playlist: 1041 | * 1042 | * spotify:user:napstersean:playlist:3vxotOnOGDlZXyzJPLFnm2 1043 | * spotify:playlist:3vxotOnOGDlZXyzJPLFnm2 1044 | * 1045 | * @param baseUri The first URI to compare. 1046 | * @param refUri The second URI to compare. 1047 | * @return Whether they shared idenitity 1048 | */ 1049 | static isSameIdentity(baseUri: URI | string, refUri: URI | string): boolean; 1050 | 1051 | /** 1052 | * Returns the hex representation of a Base62 encoded id. 1053 | * 1054 | * @param id The base62 encoded id. 1055 | * @return The hex representation of the base62 id. 1056 | */ 1057 | static idToHex(id: string): string; 1058 | 1059 | /** 1060 | * Returns the base62 representation of a hex encoded id. 1061 | * 1062 | * @param hex The hex encoded id. 1063 | * @return The base62 representation of the id. 1064 | */ 1065 | static hexToId(hex: string): string; 1066 | 1067 | /** 1068 | * Creates a new 'album' type URI. 1069 | * 1070 | * @param id The id of the album. 1071 | * @param disc The disc number of the album. 1072 | * @return The album URI. 1073 | */ 1074 | static albumURI(id: string, disc: number): URI; 1075 | 1076 | /** 1077 | * Creates a new 'application' type URI. 1078 | * 1079 | * @param id The id of the application. 1080 | * @param args An array containing the arguments to the app. 1081 | * @return The application URI. 1082 | */ 1083 | static applicationURI(id: string, args: string[]): URI; 1084 | 1085 | /** 1086 | * Creates a new 'artist' type URI. 1087 | * 1088 | * @param id The id of the artist. 1089 | * @return The artist URI. 1090 | */ 1091 | static artistURI(id: string): URI; 1092 | 1093 | /** 1094 | * Creates a new 'collection' type URI. 1095 | * 1096 | * @param username The non-canonical username of the rootlist owner. 1097 | * @param category The category of the collection. 1098 | * @return The collection URI. 1099 | */ 1100 | static collectionURI(username: string, category: string): URI; 1101 | 1102 | /** 1103 | * Creates a new 'collection-album' type URI. 1104 | * 1105 | * @param username The non-canonical username of the rootlist owner. 1106 | * @param id The id of the album. 1107 | * @return The collection album URI. 1108 | */ 1109 | static collectionAlbumURI(username: string, id: string): URI; 1110 | 1111 | /** 1112 | * Creates a new 'collection-artist' type URI. 1113 | * 1114 | * @param username The non-canonical username of the rootlist owner. 1115 | * @param id The id of the artist. 1116 | * @return The collection artist URI. 1117 | */ 1118 | static collectionAlbumURI(username: string, id: string): URI; 1119 | 1120 | /** 1121 | * Creates a new 'concert' type URI. 1122 | * 1123 | * @param id The id of the concert. 1124 | * @return The concert URI. 1125 | */ 1126 | static concertURI(id: string): URI; 1127 | 1128 | /** 1129 | * Creates a new 'episode' type URI. 1130 | * 1131 | * @param id The id of the episode. 1132 | * @return The episode URI. 1133 | */ 1134 | static episodeURI(id: string): URI; 1135 | 1136 | /** 1137 | * Creates a new 'folder' type URI. 1138 | * 1139 | * @param id The id of the folder. 1140 | * @return The folder URI. 1141 | */ 1142 | static folderURI(id: string): URI; 1143 | 1144 | /** 1145 | * Creates a new 'local-album' type URI. 1146 | * 1147 | * @param artist The artist of the album. 1148 | * @param album The name of the album. 1149 | * @return The local album URI. 1150 | */ 1151 | static localAlbumURI(artist: string, album: string): URI; 1152 | 1153 | /** 1154 | * Creates a new 'local-artist' type URI. 1155 | * 1156 | * @param artist The name of the artist. 1157 | * @return The local artist URI. 1158 | */ 1159 | static localArtistURI(artist: string): URI; 1160 | 1161 | /** 1162 | * Creates a new 'playlist-v2' type URI. 1163 | * 1164 | * @param id The id of the playlist. 1165 | * @return The playlist URI. 1166 | */ 1167 | static playlistV2URI(id: string): URI; 1168 | 1169 | /** 1170 | * Creates a new 'prerelease' type URI. 1171 | * 1172 | * @param id The id of the prerelease. 1173 | * @return The prerelease URI. 1174 | */ 1175 | static prereleaseURI(id: string): URI; 1176 | 1177 | /** 1178 | * Creates a new 'profile' type URI. 1179 | * 1180 | * @param username The non-canonical username of the rootlist owner. 1181 | * @param args A list of arguments. 1182 | * @return The profile URI. 1183 | */ 1184 | static profileURI(username: string, args: string[]): URI; 1185 | 1186 | /** 1187 | * Creates a new 'search' type URI. 1188 | * 1189 | * @param query The unencoded search query. 1190 | * @return The search URI 1191 | */ 1192 | static searchURI(query: string): URI; 1193 | 1194 | /** 1195 | * Creates a new 'show' type URI. 1196 | * 1197 | * @param id The id of the show. 1198 | * @return The show URI. 1199 | */ 1200 | static showURI(id: string): URI; 1201 | 1202 | /** 1203 | * Creates a new 'station' type URI. 1204 | * 1205 | * @param args An array of arguments for the station. 1206 | * @return The station URI. 1207 | */ 1208 | static stationURI(args: string[]): URI; 1209 | 1210 | /** 1211 | * Creates a new 'track' type URI. 1212 | * 1213 | * @param id The id of the track. 1214 | * @param anchor The point in the track formatted as mm:ss 1215 | * @param context An optional context URI 1216 | * @param play Toggles autoplay 1217 | * @return The track URI. 1218 | */ 1219 | static trackURI( 1220 | id: string, 1221 | anchor: string, 1222 | context?: string, 1223 | play?: boolean, 1224 | ): URI; 1225 | 1226 | /** 1227 | * Creates a new 'user-toplist' type URI. 1228 | * 1229 | * @param username The non-canonical username of the toplist owner. 1230 | * @param toplist The toplist type. 1231 | * @return The user-toplist URI. 1232 | */ 1233 | static userToplistURI(username: string, toplist: string): URI; 1234 | 1235 | static isAd(uri: URI | string): boolean; 1236 | static isAlbum(uri: URI | string): boolean; 1237 | static isGenre(uri: URI | string): boolean; 1238 | static isQueue(uri: URI | string): boolean; 1239 | static isApplication(uri: URI | string): boolean; 1240 | static isArtist(uri: URI | string): boolean; 1241 | static isArtistToplist(uri: URI | string): boolean; 1242 | static isArtistConcerts(uri: URI | string): boolean; 1243 | static isAudioFile(uri: URI | string): boolean; 1244 | static isCollection(uri: URI | string): boolean; 1245 | static isCollectionAlbum(uri: URI | string): boolean; 1246 | static isCollectionArtist(uri: URI | string): boolean; 1247 | static isCollectionMissingAlbum(uri: URI | string): boolean; 1248 | static isCollectionTrackList(uri: URI | string): boolean; 1249 | static isConcert(uri: URI | string): boolean; 1250 | static isContextGroup(uri: URI | string): boolean; 1251 | static isDailyMix(uri: URI | string): boolean; 1252 | static isEmpty(uri: URI | string): boolean; 1253 | static isEpisode(uri: URI | string): boolean; 1254 | static isFacebook(uri: URI | string): boolean; 1255 | static isFolder(uri: URI | string): boolean; 1256 | static isFollowers(uri: URI | string): boolean; 1257 | static isFollowing(uri: URI | string): boolean; 1258 | static isImage(uri: URI | string): boolean; 1259 | static isInbox(uri: URI | string): boolean; 1260 | static isInterruption(uri: URI | string): boolean; 1261 | static isLibrary(uri: URI | string): boolean; 1262 | static isLive(uri: URI | string): boolean; 1263 | static isRoom(uri: URI | string): boolean; 1264 | static isExpression(uri: URI | string): boolean; 1265 | static isLocal(uri: URI | string): boolean; 1266 | static isLocalTrack(uri: URI | string): boolean; 1267 | static isLocalAlbum(uri: URI | string): boolean; 1268 | static isLocalArtist(uri: URI | string): boolean; 1269 | static isMerch(uri: URI | string): boolean; 1270 | static isMosaic(uri: URI | string): boolean; 1271 | static isPlaylist(uri: URI | string): boolean; 1272 | static isPlaylistV2(uri: URI | string): boolean; 1273 | static isPrerelease(uri: URI | string): boolean; 1274 | static isProfile(uri: URI | string): boolean; 1275 | static isPublishedRootlist(uri: URI | string): boolean; 1276 | static isRadio(uri: URI | string): boolean; 1277 | static isRootlist(uri: URI | string): boolean; 1278 | static isSearch(uri: URI | string): boolean; 1279 | static isShow(uri: URI | string): boolean; 1280 | static isSocialSession(uri: URI | string): boolean; 1281 | static isSpecial(uri: URI | string): boolean; 1282 | static isStarred(uri: URI | string): boolean; 1283 | static isStation(uri: URI | string): boolean; 1284 | static isTempPlaylist(uri: URI | string): boolean; 1285 | static isToplist(uri: URI | string): boolean; 1286 | static isTrack(uri: URI | string): boolean; 1287 | static isTrackset(uri: URI | string): boolean; 1288 | static isUserToplist(uri: URI | string): boolean; 1289 | static isUserTopTracks(uri: URI | string): boolean; 1290 | static isUnknown(uri: URI | string): boolean; 1291 | static isMedia(uri: URI | string): boolean; 1292 | static isQuestion(uri: URI | string): boolean; 1293 | static isPoll(uri: URI | string): boolean; 1294 | static isPlaylistV1OrV2(uri: URI | string): boolean; 1295 | } 1296 | 1297 | /** 1298 | * Create custom menu item and prepend to right click context menu 1299 | */ 1300 | namespace ContextMenu { 1301 | type OnClickCallback = ( 1302 | uris: string[], 1303 | uids?: string[], 1304 | contextUri?: string, 1305 | ) => void; 1306 | type ShouldAddCallback = ( 1307 | uris: string[], 1308 | uids?: string[], 1309 | contextUri?: string, 1310 | ) => boolean; 1311 | 1312 | // Single context menu item 1313 | class Item { 1314 | /** 1315 | * List of valid icons to use. 1316 | */ 1317 | static readonly iconList: Icon[]; 1318 | constructor( 1319 | name: string, 1320 | onClick: OnClickCallback, 1321 | shouldAdd?: ShouldAddCallback, 1322 | icon?: Icon, 1323 | disabled?: boolean, 1324 | ); 1325 | name: string; 1326 | icon: Icon | string; 1327 | disabled: boolean; 1328 | /** 1329 | * A function returning boolean determines whether item should be prepended. 1330 | */ 1331 | shouldAdd: ShouldAddCallback; 1332 | /** 1333 | * A function to call when item is clicked 1334 | */ 1335 | onClick: OnClickCallback; 1336 | /** 1337 | * Item is only available in Context Menu when method "register" is called. 1338 | */ 1339 | register: () => void; 1340 | /** 1341 | * Stop Item to be prepended into Context Menu. 1342 | */ 1343 | deregister: () => void; 1344 | } 1345 | 1346 | /** 1347 | * Create a sub menu to contain `Item`s. 1348 | * `Item`s in `subItems` array shouldn't be registered. 1349 | */ 1350 | class SubMenu { 1351 | constructor( 1352 | name: string, 1353 | subItems: Iterable, 1354 | shouldAdd?: ShouldAddCallback, 1355 | disabled?: boolean, 1356 | ); 1357 | name: string; 1358 | disabled: boolean; 1359 | /** 1360 | * A function returning boolean determines whether item should be prepended. 1361 | */ 1362 | shouldAdd: ShouldAddCallback; 1363 | addItem: (item: Item) => void; 1364 | removeItem: (item: Item) => void; 1365 | /** 1366 | * SubMenu is only available in Context Menu when method "register" is called. 1367 | */ 1368 | register: () => void; 1369 | /** 1370 | * Stop SubMenu to be prepended into Context Menu. 1371 | */ 1372 | deregister: () => void; 1373 | } 1374 | } 1375 | 1376 | /** 1377 | * Popup Modal 1378 | */ 1379 | namespace PopupModal { 1380 | interface Content { 1381 | title: string; 1382 | /** 1383 | * You can specify a string for simple text display 1384 | * or a HTML element for interactive config/setting menu 1385 | */ 1386 | content: string | Element; 1387 | /** 1388 | * Bigger window 1389 | */ 1390 | isLarge?: boolean; 1391 | } 1392 | 1393 | function display(e: Content): void; 1394 | function hide(): void; 1395 | } 1396 | 1397 | /** React instance to create components */ 1398 | const React: any; 1399 | /** React DOM instance to render and mount components */ 1400 | const ReactDOM: any; 1401 | /** React DOM Server instance to render components to string */ 1402 | const ReactDOMServer: any; 1403 | 1404 | /** Stock React components exposed from Spotify library */ 1405 | namespace ReactComponent { 1406 | type ContextMenuProps = { 1407 | /** 1408 | * Decide whether to use the global singleton context menu (rendered in ) 1409 | * or a new inline context menu (rendered in a sibling 1410 | * element to `children`) 1411 | */ 1412 | renderInline?: boolean; 1413 | /** 1414 | * Determins what will trigger the context menu. For example, a click, or a right-click 1415 | */ 1416 | trigger?: 'click' | 'right-click'; 1417 | /** 1418 | * Determins is the context menu should open or toggle when triggered 1419 | */ 1420 | action?: 'toggle' | 'open'; 1421 | /** 1422 | * The preferred placement of the context menu when it opens. 1423 | * Relative to trigger element. 1424 | */ 1425 | placement?: 1426 | | 'top' 1427 | | 'top-start' 1428 | | 'top-end' 1429 | | 'right' 1430 | | 'right-start' 1431 | | 'right-end' 1432 | | 'bottom' 1433 | | 'bottom-start' 1434 | | 'bottom-end' 1435 | | 'left' 1436 | | 'left-start' 1437 | | 'left-end'; 1438 | /** 1439 | * The x and y offset distances at which the context menu should open. 1440 | * Relative to trigger element and `position`. 1441 | */ 1442 | offset?: [number, number]; 1443 | /** 1444 | * Will stop the client from scrolling while the context menu is open 1445 | */ 1446 | preventScrollingWhileOpen?: boolean; 1447 | /** 1448 | * The menu UI to render inside of the context menu. 1449 | */ 1450 | menu: 1451 | | typeof Spicetify.ReactComponent.Menu 1452 | | typeof Spicetify.ReactComponent.AlbumMenu 1453 | | typeof Spicetify.ReactComponent.PodcastShowMenu 1454 | | typeof Spicetify.ReactComponent.ArtistMenu 1455 | | typeof Spicetify.ReactComponent.PlaylistMenu; 1456 | /** 1457 | * A child of the context menu. Should be `