Qobuz Linkifier

make templates copy links to clipboard

  1. // ==UserScript==
  2. // @name Qobuz Linkifier
  3. // @version 0.1.1
  4. // @description make templates copy links to clipboard
  5. // @author You
  6. // @match https://www.qobuz.com/*/shop
  7. // @match https://www.qobuz.com/*/shop/*/*
  8. // @match https://www.qobuz.com/*/search*
  9. // @match https://www.qobuz.com/*/label/*/*/*
  10. // @match https://www.qobuz.com/*/interpreter/*/*
  11. // @match https://www.qobuz.com/*/album/*/*
  12. // @match https://www.qobuz.com/*/playlists/*/*
  13. // @match https://www.qobuz.com/*/genre/*/*
  14. // @icon https://www.qobuz.com/favicon.ico
  15. // @grant none
  16. // @namespace https://greasyfork.org/users/1465219
  17. // ==/UserScript==
  18.  
  19. (function () {
  20. 'use strict';
  21.  
  22. const BASE_URL = 'https://play.qobuz.com/',
  23. LOAD_MORE_TRACKS_DELAY = 500,
  24. COPY_MESSAGE_DISPLAY_DURATION = 1500,
  25. DEFAULT_LOC = { lang: 'en', country: '_' };
  26.  
  27. addCustomStyle(`
  28. .store-wallpaper,
  29. .album-addtocart,
  30. .player__ad,
  31. .store-cart,
  32. .shop-cart,
  33. .price-box .price,
  34. .on-sale {
  35. display: none !important;
  36. }
  37.  
  38. #store-search {
  39. padding-right: 0 !important;
  40. border-right: none !important
  41. }
  42.  
  43. .shop-search {
  44. margin-right: 16px !important;
  45. }
  46.  
  47. .track__item--button span.pct {
  48. position: absolute !important;
  49. top: 8px !important;
  50. left: 5px !important;
  51. }
  52.  
  53. .catalog-heading__button span.pct {
  54. margin-right: 18px;
  55. font-size: 12px;
  56. font-weight: 700;
  57. }
  58.  
  59. .product__button span.pct {
  60. left: 11px;
  61. position: absolute;
  62. }
  63.  
  64. .player__webplayer span.pct {
  65. margin-right: 8px;
  66. }
  67.  
  68. .player__webplayer span:last-child {
  69. padding-top: 2px;
  70. }
  71.  
  72. .no-wrap {
  73. white-space: nowrap !important;
  74. }
  75.  
  76. .color-white {
  77. color: #FFF !important;
  78. }
  79. `);
  80.  
  81. const templates = infuseTemplates(window.location.pathname.split('/')[1].split('-').reverse(), {
  82. en: {
  83. _: {
  84. label: 'label',
  85. artist: 'artist',
  86. album: 'album',
  87. playlist: 'playlist',
  88. track: 'track',
  89. copyLink: function (type) {
  90. return type ? `Copy ${this[type]} link` : 'Copy link'
  91. },
  92. copyLinkWithHighlight: function (type) {
  93. return `<span class="color-white">Copy</span> ${this[type]} link`
  94. },
  95. linkCopied: function (type) {
  96. return type ? `${this[type].toTitleCase()} link copied!` : 'Link copied!'
  97. },
  98. linkCopiedWithHighlight: function (type) {
  99. return `<span class="color-white">${this[type].toTitleCase()} link</span> copied!`
  100. },
  101. },
  102. get uk() { return this._ },
  103. get ie() { return this._ },
  104. get us() { return this._ },
  105. get au() { return this._ },
  106. get ca() { return this._ },
  107. get nz() { return this._ },
  108. get dk() { return this._ },
  109. get fi() { return this._ },
  110. get se() { return this._ },
  111. get no() { return this._ }
  112. },
  113. de: {
  114. _: {
  115. label: 'Verlag',
  116. artist: 'Interpret',
  117. album: 'Album',
  118. playlist: 'Wiedergabeliste',
  119. track: 'Titel',
  120. copyLink: function (type) { return type ? `${this[type]}-Link kopieren` : 'Link kopieren' },
  121. copyLinkWithHighlight: function (type) {
  122. return `<span class="color-white">${this[type]}-Link</span> kopieren`
  123. },
  124. linkCopied: function (type) {
  125. return type ? `${this[type]}-Link kopiert!` : 'Link kopiert!'
  126. },
  127. linkCopiedWithHighlight: function (type) {
  128. return `<span class="color-white">${this[type]}-Link</span> kopiert!`
  129. },
  130. },
  131. get de() { return this._ },
  132. get at() { return this._ },
  133. get ch() { return this._ },
  134. get lu() { return this._ }
  135. },
  136. /* es: {
  137. _: {
  138. },
  139. get es() { return this._ },
  140. get mx() { return this._ },
  141. get ar() { return this._ },
  142. get cl() { return this._ },
  143. get co() { return this._ }
  144. },
  145. pt: {
  146. get pt() { return this._ },
  147. get br() { return this._ }
  148. },
  149. nl: {
  150. get nl() { return this._ },
  151. get be() { return this._ }
  152. },
  153. fr: {
  154. get fr() { return this._ },
  155. get ch() { return this._ },
  156. get lu() { return this._ },
  157. get be() { return this._ },
  158. get ca() { return this._ }
  159. },
  160. it: {
  161. get it() { return this._ }
  162. } */
  163. }, (data) => ({
  164. track: {
  165. content: () => `
  166. <span class="pct pct pct-edit"></span>
  167. <span class="no-wrap">${data.copyLink()}</span>
  168. `,
  169. message: () => `
  170. <span class="pct pct pct-checkbox"></span>
  171. <span class="no-wrap">${data.linkCopied()}</span>
  172. `
  173. },
  174. albumGrid: {
  175. content: () => `
  176. <span class="pct pct pct-edit"></span>
  177. <span class="product__button--highlight">
  178. ${data.copyLinkWithHighlight('album')}
  179. </span>
  180. `,
  181. message: () => `
  182. <span class="pct pct pct-checkbox"></span>
  183. <span class="product__button--highlight">
  184. ${data.linkCopiedWithHighlight('album')}</span>
  185. </span>
  186. `
  187. },
  188. search: {
  189. content: () => `
  190. <span class="no-wrap">${data.copyLink('album')}</span>
  191. `,
  192. message: () => `
  193. <span class="no-wrap">${data.linkCopied('album')}</span>
  194. `
  195. },
  196. main: {
  197. content: (type) => `
  198. <span class="pct pct pct-edit"></span>
  199. <span class="no-wrap">${data.copyLink(type)}</span>
  200. `,
  201. message: (type) => `
  202. <span class="pct pct pct-checkbox"></span>
  203. <span class="no-wrap">${data.linkCopied(type)}</span>
  204. `
  205. }
  206. }));
  207.  
  208. templates["main"].content("album")
  209.  
  210. const selectors = {
  211. main: '.catalog-heading__button, .player__webplayer',
  212. search: '.btn__qobuz.btn__qobuz--see-album',
  213. album: '.product__button.add_to_cart',
  214. track: '.track__item.track__item--button',
  215. loadMore: '.player-more'
  216. }
  217.  
  218. const mainButton = document.querySelector(selectors.main);
  219.  
  220. if (mainButton) {
  221. const [type, id] = mainButton.attributes.href.value.split('/').slice(-2);
  222.  
  223. replaceButton(mainButton, id, type, 'main');
  224.  
  225. document.querySelectorAll(selectors.track).forEach((el, i) => {
  226. if (el.classList.contains('track__unavailable')) return;
  227.  
  228. const trackButton = replaceButton(
  229. el,
  230. el.dataset.url.split('/').slice(-1)[0],
  231. 'track'
  232. );
  233.  
  234. trackButton.addEventListener('dblclick', ev => { ev.preventDefault(); ev.stopPropagation(); })
  235. });
  236. }
  237.  
  238. document.querySelectorAll(selectors.album).forEach((el, i) => {
  239. replaceButton(
  240. el,
  241. el.dataset.url.split('/').slice(-1)[0],
  242. 'album',
  243. 'albumGrid'
  244. );
  245. });
  246.  
  247. document.querySelectorAll(selectors.search).forEach((el, i) => {
  248. replaceButton(
  249. el,
  250. el.attributes.href.value.split('/').slice(-1)[0],
  251. 'album',
  252. 'search'
  253. );
  254. });
  255.  
  256. const loadMore = document.querySelector(selectors.loadMore)
  257.  
  258. if (loadMore) {
  259. loadMore.addEventListener('click', ev => {
  260. setTimeout(() => {
  261. document.querySelectorAll(selectors.track).forEach((el, i) => {
  262. if (el.classList.contains('track__unavailable')) return;
  263.  
  264. const trackButton = replaceButton(
  265. el,
  266. el.dataset.url.split('/').slice(-1)[0],
  267. 'track'
  268. );
  269.  
  270. trackButton.addEventListener('dblclick', ev => { ev.preventDefault(); ev.stopPropagation(); })
  271. });
  272. }, LOAD_MORE_TRACKS_DELAY);
  273. })
  274. }
  275.  
  276. function replaceButton(button, id, type, contentType = type) {
  277. let timeout;
  278.  
  279. const content = templates[contentType].content(type),
  280. url = BASE_URL + `${type}/${id}`,
  281. newButton = button.cloneNode(true);
  282.  
  283. newButton.setAttribute('title', url);
  284. newButton.setAttribute('href', url);
  285. newButton.innerHTML = content;
  286.  
  287. button.replaceWith(newButton);
  288. button.remove();
  289.  
  290. let wasCopied = false;
  291.  
  292. newButton.addEventListener('click', ev => {
  293. ev.preventDefault();
  294.  
  295. navigator.clipboard.writeText(url);
  296.  
  297. if (wasCopied) {
  298. clearTimeout(timeout);
  299. }
  300. else {
  301. newButton.innerHTML = templates[contentType].message(type);
  302. wasCopied = true;
  303. }
  304.  
  305. timeout = window.setTimeout(
  306. () => {
  307. newButton.innerHTML = content;
  308. wasCopied = false;
  309. },
  310. COPY_MESSAGE_DISPLAY_DURATION
  311. );
  312.  
  313. ev.stopPropagation();
  314.  
  315. return false;
  316. });
  317.  
  318. return newButton;
  319. }
  320.  
  321. function addCustomStyle(style) {
  322. document.body.append(
  323. document.createElement('style')
  324. .appendChild(
  325. document.createTextNode(style)
  326. )
  327. .parentNode
  328. );
  329. }
  330.  
  331. function infuseTemplates([country, lang], strings, templates) {
  332. return templates(strings[country]?.[lang] || strings[DEFAULT_LOC.lang][DEFAULT_LOC.country]);
  333. }
  334.  
  335. Object.defineProperty(String.prototype, "toTitleCase", {
  336. value: function () {
  337. return this[0].toUpperCase() + this.slice(1);
  338. },
  339. writable: true,
  340. configurable: true,
  341. });
  342. })();