Bandcamp script (Deluxe Edition)

A discography player for bandcamp.com and manager for your played albums

נכון ליום 09-11-2023. ראה הגרסה האחרונה.

  1. // ==UserScript==
  2. // @name Bandcamp script (Deluxe Edition)
  3. // @description A discography player for bandcamp.com and manager for your played albums
  4. // @namespace https://openuserjs.org/users/cuzi
  5. // @supportURL https://github.com/cvzi/Bandcamp-script-deluxe-edition/issues
  6. // @icon https://raw.githubusercontent.com/cvzi/Bandcamp-script-deluxe-edition/master/images/icon.png
  7. // @contributionURL https://github.com/cvzi/Bandcamp-script-deluxe-edition#donate
  8. // @require https://unpkg.com/json5@2.1.0/dist/index.min.js
  9. // @require https://openuserjs.org/src/libs/cuzi/GeniusLyrics.js
  10. // @require https://unpkg.com/react@18/umd/react.development.js
  11. // @require https://unpkg.com/react-dom@18/umd/react-dom.development.js
  12. // @run-at document-start
  13. // @match https://*/*
  14. // @match https://bandcamp.com/*
  15. // @exclude https://bandcamp.com/videoframe*
  16. // @exclude https://bandcamp.com/EmbeddedPlayer*
  17. // @connect bandcamp.com
  18. // @connect *.bandcamp.com
  19. // @connect bcbits.com
  20. // @connect *.bcbits.com
  21. // @connect genius.com
  22. // @connect *
  23. // @version 1.31.1
  24. // @homepage https://github.com/cvzi/Bandcamp-script-deluxe-edition
  25. // @author cuzi
  26. // @license MIT
  27. // @grant GM.xmlHttpRequest
  28. // @grant GM.setValue
  29. // @grant GM.getValue
  30. // @grant GM.notification
  31. // @grant GM_download
  32. // @grant GM.registerMenuCommand
  33. // @grant GM_registerMenuCommand
  34. // @grant GM_addStyle
  35. // @grant GM_setClipboard
  36. // @grant unsafeWindow
  37. // ==/UserScript==
  38.  
  39. // ==OpenUserJS==
  40. // @author cuzi
  41. // ==/OpenUserJS==
  42.  
  43. /*
  44. MIT License
  45.  
  46. Copyright (c) 2020 cvzi
  47.  
  48. Permission is hereby granted, free of charge, to any person obtaining a copy
  49. of this software and associated documentation files (the "Software"), to deal
  50. in the Software without restriction, including without limitation the rights
  51. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  52. copies of the Software, and to permit persons to whom the Software is
  53. furnished to do so, subject to the following conditions:
  54.  
  55. The above copyright notice and this permission notice shall be included in all
  56. copies or substantial portions of the Software.
  57.  
  58. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  59. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  60. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  61. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  62. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  63. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  64. SOFTWARE.
  65. */
  66.  
  67. /* globals React, ReactDOM */
  68. (function (React, ReactDOM) {
  69. 'use strict';
  70.  
  71. function _interopNamespaceDefault(e) {
  72. var n = Object.create(null);
  73. if (e) {
  74. Object.keys(e).forEach(function (k) {
  75. if (k !== 'default') {
  76. var d = Object.getOwnPropertyDescriptor(e, k);
  77. Object.defineProperty(n, k, d.get ? d : {
  78. enumerable: true,
  79. get: function () { return e[k]; }
  80. });
  81. }
  82. });
  83. }
  84. n.default = e;
  85. return Object.freeze(n);
  86. }
  87.  
  88. var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
  89. var ReactDOM__namespace = /*#__PURE__*/_interopNamespaceDefault(ReactDOM);
  90.  
  91. /*
  92. Compatibility adaptions for Violentmonkey https://github.com/violentmonkey/violentmonkey
  93. */
  94.  
  95. if (typeof GM.registerMenuCommand !== 'function') {
  96. if (typeof GM_registerMenuCommand === 'function') {
  97. GM.registerMenuCommand = GM_registerMenuCommand;
  98. } else {
  99. console.warn('Neither GM.registerMenuCommand nor GM_registerMenuCommand are available');
  100. }
  101. }
  102.  
  103. function _defineProperty(obj, key, value) {
  104. key = _toPropertyKey(key);
  105. if (key in obj) {
  106. Object.defineProperty(obj, key, {
  107. value: value,
  108. enumerable: true,
  109. configurable: true,
  110. writable: true
  111. });
  112. } else {
  113. obj[key] = value;
  114. }
  115. return obj;
  116. }
  117. function _toPrimitive(input, hint) {
  118. if (typeof input !== "object" || input === null) return input;
  119. var prim = input[Symbol.toPrimitive];
  120. if (prim !== undefined) {
  121. var res = prim.call(input, hint || "default");
  122. if (typeof res !== "object") return res;
  123. throw new TypeError("@@toPrimitive must return a primitive value.");
  124. }
  125. return (hint === "string" ? String : Number)(input);
  126. }
  127. function _toPropertyKey(arg) {
  128. var key = _toPrimitive(arg, "string");
  129. return typeof key === "symbol" ? key : String(key);
  130. }
  131.  
  132. function _extends() {
  133. _extends = Object.assign ? Object.assign.bind() : function (target) {
  134. for (var i = 1; i < arguments.length; i++) {
  135. var source = arguments[i];
  136. for (var key in source) {
  137. if (Object.prototype.hasOwnProperty.call(source, key)) {
  138. target[key] = source[key];
  139. }
  140. }
  141. }
  142. return target;
  143. };
  144. return _extends.apply(this, arguments);
  145. }
  146.  
  147. function _assertThisInitialized(self) {
  148. if (self === void 0) {
  149. throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  150. }
  151. return self;
  152. }
  153.  
  154. function _setPrototypeOf(o, p) {
  155. _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
  156. o.__proto__ = p;
  157. return o;
  158. };
  159. return _setPrototypeOf(o, p);
  160. }
  161.  
  162. function _inheritsLoose(subClass, superClass) {
  163. subClass.prototype = Object.create(superClass.prototype);
  164. subClass.prototype.constructor = subClass;
  165. _setPrototypeOf(subClass, superClass);
  166. }
  167.  
  168. var safeIsNaN = Number.isNaN || function ponyfill(value) {
  169. return typeof value === 'number' && value !== value;
  170. };
  171. function isEqual(first, second) {
  172. if (first === second) {
  173. return true;
  174. }
  175. if (safeIsNaN(first) && safeIsNaN(second)) {
  176. return true;
  177. }
  178. return false;
  179. }
  180. function areInputsEqual(newInputs, lastInputs) {
  181. if (newInputs.length !== lastInputs.length) {
  182. return false;
  183. }
  184. for (var i = 0; i < newInputs.length; i++) {
  185. if (!isEqual(newInputs[i], lastInputs[i])) {
  186. return false;
  187. }
  188. }
  189. return true;
  190. }
  191. function memoizeOne(resultFn, isEqual) {
  192. if (isEqual === void 0) {
  193. isEqual = areInputsEqual;
  194. }
  195. var lastThis;
  196. var lastArgs = [];
  197. var lastResult;
  198. var calledOnce = false;
  199. function memoized() {
  200. var newArgs = [];
  201. for (var _i = 0; _i < arguments.length; _i++) {
  202. newArgs[_i] = arguments[_i];
  203. }
  204. if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) {
  205. return lastResult;
  206. }
  207. lastResult = resultFn.apply(this, newArgs);
  208. calledOnce = true;
  209. lastThis = this;
  210. lastArgs = newArgs;
  211. return lastResult;
  212. }
  213. return memoized;
  214. }
  215.  
  216. // Animation frame based implementation of setTimeout.
  217. // Inspired by Joe Lambert, https://gist.github.com/joelambert/1002116#file-requesttimeout-js
  218. var hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function';
  219. var now = hasNativePerformanceNow ? function () {
  220. return performance.now();
  221. } : function () {
  222. return Date.now();
  223. };
  224. function cancelTimeout(timeoutID) {
  225. cancelAnimationFrame(timeoutID.id);
  226. }
  227. function requestTimeout(callback, delay) {
  228. var start = now();
  229. function tick() {
  230. if (now() - start >= delay) {
  231. callback.call(null);
  232. } else {
  233. timeoutID.id = requestAnimationFrame(tick);
  234. }
  235. }
  236. var timeoutID = {
  237. id: requestAnimationFrame(tick)
  238. };
  239. return timeoutID;
  240. }
  241. var size = -1; // This utility copied from "dom-helpers" package.
  242.  
  243. function getScrollbarSize(recalculate) {
  244. if (recalculate === void 0) {
  245. recalculate = false;
  246. }
  247. if (size === -1 || recalculate) {
  248. var div = document.createElement('div');
  249. var style = div.style;
  250. style.width = '50px';
  251. style.height = '50px';
  252. style.overflow = 'scroll';
  253. document.body.appendChild(div);
  254. size = div.offsetWidth - div.clientWidth;
  255. document.body.removeChild(div);
  256. }
  257. return size;
  258. }
  259. var cachedRTLResult = null; // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
  260. // Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left).
  261. // Safari's elastic bounce makes detecting this even more complicated wrt potential false positives.
  262. // The safest way to check this is to intentionally set a negative offset,
  263. // and then verify that the subsequent "scroll" event matches the negative offset.
  264. // If it does not match, then we can assume a non-standard RTL scroll implementation.
  265.  
  266. function getRTLOffsetType(recalculate) {
  267. if (recalculate === void 0) {
  268. recalculate = false;
  269. }
  270. if (cachedRTLResult === null || recalculate) {
  271. var outerDiv = document.createElement('div');
  272. var outerStyle = outerDiv.style;
  273. outerStyle.width = '50px';
  274. outerStyle.height = '50px';
  275. outerStyle.overflow = 'scroll';
  276. outerStyle.direction = 'rtl';
  277. var innerDiv = document.createElement('div');
  278. var innerStyle = innerDiv.style;
  279. innerStyle.width = '100px';
  280. innerStyle.height = '100px';
  281. outerDiv.appendChild(innerDiv);
  282. document.body.appendChild(outerDiv);
  283. if (outerDiv.scrollLeft > 0) {
  284. cachedRTLResult = 'positive-descending';
  285. } else {
  286. outerDiv.scrollLeft = 1;
  287. if (outerDiv.scrollLeft === 0) {
  288. cachedRTLResult = 'negative';
  289. } else {
  290. cachedRTLResult = 'positive-ascending';
  291. }
  292. }
  293. document.body.removeChild(outerDiv);
  294. return cachedRTLResult;
  295. }
  296. return cachedRTLResult;
  297. }
  298. var IS_SCROLLING_DEBOUNCE_INTERVAL$1 = 150;
  299. var defaultItemKey$1 = function defaultItemKey(index, data) {
  300. return index;
  301. }; // In DEV mode, this Set helps us only log a warning once per component instance.
  302. function createListComponent(_ref) {
  303. var _class;
  304. var getItemOffset = _ref.getItemOffset,
  305. getEstimatedTotalSize = _ref.getEstimatedTotalSize,
  306. getItemSize = _ref.getItemSize,
  307. getOffsetForIndexAndAlignment = _ref.getOffsetForIndexAndAlignment,
  308. getStartIndexForOffset = _ref.getStartIndexForOffset,
  309. getStopIndexForStartIndex = _ref.getStopIndexForStartIndex,
  310. initInstanceProps = _ref.initInstanceProps,
  311. shouldResetStyleCacheOnItemSizeChange = _ref.shouldResetStyleCacheOnItemSizeChange,
  312. validateProps = _ref.validateProps;
  313. return _class = /*#__PURE__*/function (_PureComponent) {
  314. _inheritsLoose(List, _PureComponent);
  315.  
  316. // Always use explicit constructor for React components.
  317. // It produces less code after transpilation. (#26)
  318. // eslint-disable-next-line no-useless-constructor
  319. function List(props) {
  320. var _this;
  321. _this = _PureComponent.call(this, props) || this;
  322. _this._instanceProps = initInstanceProps(_this.props, _assertThisInitialized(_this));
  323. _this._outerRef = void 0;
  324. _this._resetIsScrollingTimeoutId = null;
  325. _this.state = {
  326. instance: _assertThisInitialized(_this),
  327. isScrolling: false,
  328. scrollDirection: 'forward',
  329. scrollOffset: typeof _this.props.initialScrollOffset === 'number' ? _this.props.initialScrollOffset : 0,
  330. scrollUpdateWasRequested: false
  331. };
  332. _this._callOnItemsRendered = void 0;
  333. _this._callOnItemsRendered = memoizeOne(function (overscanStartIndex, overscanStopIndex, visibleStartIndex, visibleStopIndex) {
  334. return _this.props.onItemsRendered({
  335. overscanStartIndex: overscanStartIndex,
  336. overscanStopIndex: overscanStopIndex,
  337. visibleStartIndex: visibleStartIndex,
  338. visibleStopIndex: visibleStopIndex
  339. });
  340. });
  341. _this._callOnScroll = void 0;
  342. _this._callOnScroll = memoizeOne(function (scrollDirection, scrollOffset, scrollUpdateWasRequested) {
  343. return _this.props.onScroll({
  344. scrollDirection: scrollDirection,
  345. scrollOffset: scrollOffset,
  346. scrollUpdateWasRequested: scrollUpdateWasRequested
  347. });
  348. });
  349. _this._getItemStyle = void 0;
  350. _this._getItemStyle = function (index) {
  351. var _this$props = _this.props,
  352. direction = _this$props.direction,
  353. itemSize = _this$props.itemSize,
  354. layout = _this$props.layout;
  355. var itemStyleCache = _this._getItemStyleCache(shouldResetStyleCacheOnItemSizeChange && itemSize, shouldResetStyleCacheOnItemSizeChange && layout, shouldResetStyleCacheOnItemSizeChange && direction);
  356. var style;
  357. if (itemStyleCache.hasOwnProperty(index)) {
  358. style = itemStyleCache[index];
  359. } else {
  360. var _offset = getItemOffset(_this.props, index, _this._instanceProps);
  361. var size = getItemSize(_this.props, index, _this._instanceProps); // TODO Deprecate direction "horizontal"
  362.  
  363. var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
  364. var isRtl = direction === 'rtl';
  365. var offsetHorizontal = isHorizontal ? _offset : 0;
  366. itemStyleCache[index] = style = {
  367. position: 'absolute',
  368. left: isRtl ? undefined : offsetHorizontal,
  369. right: isRtl ? offsetHorizontal : undefined,
  370. top: !isHorizontal ? _offset : 0,
  371. height: !isHorizontal ? size : '100%',
  372. width: isHorizontal ? size : '100%'
  373. };
  374. }
  375. return style;
  376. };
  377. _this._getItemStyleCache = void 0;
  378. _this._getItemStyleCache = memoizeOne(function (_, __, ___) {
  379. return {};
  380. });
  381. _this._onScrollHorizontal = function (event) {
  382. var _event$currentTarget = event.currentTarget,
  383. clientWidth = _event$currentTarget.clientWidth,
  384. scrollLeft = _event$currentTarget.scrollLeft,
  385. scrollWidth = _event$currentTarget.scrollWidth;
  386. _this.setState(function (prevState) {
  387. if (prevState.scrollOffset === scrollLeft) {
  388. // Scroll position may have been updated by cDM/cDU,
  389. // In which case we don't need to trigger another render,
  390. // And we don't want to update state.isScrolling.
  391. return null;
  392. }
  393. var direction = _this.props.direction;
  394. var scrollOffset = scrollLeft;
  395. if (direction === 'rtl') {
  396. // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
  397. // This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
  398. // It's also easier for this component if we convert offsets to the same format as they would be in for ltr.
  399. // So the simplest solution is to determine which browser behavior we're dealing with, and convert based on it.
  400. switch (getRTLOffsetType()) {
  401. case 'negative':
  402. scrollOffset = -scrollLeft;
  403. break;
  404. case 'positive-descending':
  405. scrollOffset = scrollWidth - clientWidth - scrollLeft;
  406. break;
  407. }
  408. } // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
  409.  
  410. scrollOffset = Math.max(0, Math.min(scrollOffset, scrollWidth - clientWidth));
  411. return {
  412. isScrolling: true,
  413. scrollDirection: prevState.scrollOffset < scrollLeft ? 'forward' : 'backward',
  414. scrollOffset: scrollOffset,
  415. scrollUpdateWasRequested: false
  416. };
  417. }, _this._resetIsScrollingDebounced);
  418. };
  419. _this._onScrollVertical = function (event) {
  420. var _event$currentTarget2 = event.currentTarget,
  421. clientHeight = _event$currentTarget2.clientHeight,
  422. scrollHeight = _event$currentTarget2.scrollHeight,
  423. scrollTop = _event$currentTarget2.scrollTop;
  424. _this.setState(function (prevState) {
  425. if (prevState.scrollOffset === scrollTop) {
  426. // Scroll position may have been updated by cDM/cDU,
  427. // In which case we don't need to trigger another render,
  428. // And we don't want to update state.isScrolling.
  429. return null;
  430. } // Prevent Safari's elastic scrolling from causing visual shaking when scrolling past bounds.
  431.  
  432. var scrollOffset = Math.max(0, Math.min(scrollTop, scrollHeight - clientHeight));
  433. return {
  434. isScrolling: true,
  435. scrollDirection: prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
  436. scrollOffset: scrollOffset,
  437. scrollUpdateWasRequested: false
  438. };
  439. }, _this._resetIsScrollingDebounced);
  440. };
  441. _this._outerRefSetter = function (ref) {
  442. var outerRef = _this.props.outerRef;
  443. _this._outerRef = ref;
  444. if (typeof outerRef === 'function') {
  445. outerRef(ref);
  446. } else if (outerRef != null && typeof outerRef === 'object' && outerRef.hasOwnProperty('current')) {
  447. outerRef.current = ref;
  448. }
  449. };
  450. _this._resetIsScrollingDebounced = function () {
  451. if (_this._resetIsScrollingTimeoutId !== null) {
  452. cancelTimeout(_this._resetIsScrollingTimeoutId);
  453. }
  454. _this._resetIsScrollingTimeoutId = requestTimeout(_this._resetIsScrolling, IS_SCROLLING_DEBOUNCE_INTERVAL$1);
  455. };
  456. _this._resetIsScrolling = function () {
  457. _this._resetIsScrollingTimeoutId = null;
  458. _this.setState({
  459. isScrolling: false
  460. }, function () {
  461. // Clear style cache after state update has been committed.
  462. // This way we don't break pure sCU for items that don't use isScrolling param.
  463. _this._getItemStyleCache(-1, null);
  464. });
  465. };
  466. return _this;
  467. }
  468. List.getDerivedStateFromProps = function getDerivedStateFromProps(nextProps, prevState) {
  469. validateSharedProps$1(nextProps, prevState);
  470. validateProps(nextProps);
  471. return null;
  472. };
  473. var _proto = List.prototype;
  474. _proto.scrollTo = function scrollTo(scrollOffset) {
  475. scrollOffset = Math.max(0, scrollOffset);
  476. this.setState(function (prevState) {
  477. if (prevState.scrollOffset === scrollOffset) {
  478. return null;
  479. }
  480. return {
  481. scrollDirection: prevState.scrollOffset < scrollOffset ? 'forward' : 'backward',
  482. scrollOffset: scrollOffset,
  483. scrollUpdateWasRequested: true
  484. };
  485. }, this._resetIsScrollingDebounced);
  486. };
  487. _proto.scrollToItem = function scrollToItem(index, align) {
  488. if (align === void 0) {
  489. align = 'auto';
  490. }
  491. var _this$props2 = this.props,
  492. itemCount = _this$props2.itemCount,
  493. layout = _this$props2.layout;
  494. var scrollOffset = this.state.scrollOffset;
  495. index = Math.max(0, Math.min(index, itemCount - 1)); // The scrollbar size should be considered when scrolling an item into view, to ensure it's fully visible.
  496. // But we only need to account for its size when it's actually visible.
  497. // This is an edge case for lists; normally they only scroll in the dominant direction.
  498.  
  499. var scrollbarSize = 0;
  500. if (this._outerRef) {
  501. var outerRef = this._outerRef;
  502. if (layout === 'vertical') {
  503. scrollbarSize = outerRef.scrollWidth > outerRef.clientWidth ? getScrollbarSize() : 0;
  504. } else {
  505. scrollbarSize = outerRef.scrollHeight > outerRef.clientHeight ? getScrollbarSize() : 0;
  506. }
  507. }
  508. this.scrollTo(getOffsetForIndexAndAlignment(this.props, index, align, scrollOffset, this._instanceProps, scrollbarSize));
  509. };
  510. _proto.componentDidMount = function componentDidMount() {
  511. var _this$props3 = this.props,
  512. direction = _this$props3.direction,
  513. initialScrollOffset = _this$props3.initialScrollOffset,
  514. layout = _this$props3.layout;
  515. if (typeof initialScrollOffset === 'number' && this._outerRef != null) {
  516. var outerRef = this._outerRef; // TODO Deprecate direction "horizontal"
  517.  
  518. if (direction === 'horizontal' || layout === 'horizontal') {
  519. outerRef.scrollLeft = initialScrollOffset;
  520. } else {
  521. outerRef.scrollTop = initialScrollOffset;
  522. }
  523. }
  524. this._callPropsCallbacks();
  525. };
  526. _proto.componentDidUpdate = function componentDidUpdate() {
  527. var _this$props4 = this.props,
  528. direction = _this$props4.direction,
  529. layout = _this$props4.layout;
  530. var _this$state = this.state,
  531. scrollOffset = _this$state.scrollOffset,
  532. scrollUpdateWasRequested = _this$state.scrollUpdateWasRequested;
  533. if (scrollUpdateWasRequested && this._outerRef != null) {
  534. var outerRef = this._outerRef; // TODO Deprecate direction "horizontal"
  535.  
  536. if (direction === 'horizontal' || layout === 'horizontal') {
  537. if (direction === 'rtl') {
  538. // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
  539. // This is not the case for all browsers though (e.g. Chrome reports values as positive, measured relative to the left).
  540. // So we need to determine which browser behavior we're dealing with, and mimic it.
  541. switch (getRTLOffsetType()) {
  542. case 'negative':
  543. outerRef.scrollLeft = -scrollOffset;
  544. break;
  545. case 'positive-ascending':
  546. outerRef.scrollLeft = scrollOffset;
  547. break;
  548. default:
  549. var clientWidth = outerRef.clientWidth,
  550. scrollWidth = outerRef.scrollWidth;
  551. outerRef.scrollLeft = scrollWidth - clientWidth - scrollOffset;
  552. break;
  553. }
  554. } else {
  555. outerRef.scrollLeft = scrollOffset;
  556. }
  557. } else {
  558. outerRef.scrollTop = scrollOffset;
  559. }
  560. }
  561. this._callPropsCallbacks();
  562. };
  563. _proto.componentWillUnmount = function componentWillUnmount() {
  564. if (this._resetIsScrollingTimeoutId !== null) {
  565. cancelTimeout(this._resetIsScrollingTimeoutId);
  566. }
  567. };
  568. _proto.render = function render() {
  569. var _this$props5 = this.props,
  570. children = _this$props5.children,
  571. className = _this$props5.className,
  572. direction = _this$props5.direction,
  573. height = _this$props5.height,
  574. innerRef = _this$props5.innerRef,
  575. innerElementType = _this$props5.innerElementType,
  576. innerTagName = _this$props5.innerTagName,
  577. itemCount = _this$props5.itemCount,
  578. itemData = _this$props5.itemData,
  579. _this$props5$itemKey = _this$props5.itemKey,
  580. itemKey = _this$props5$itemKey === void 0 ? defaultItemKey$1 : _this$props5$itemKey,
  581. layout = _this$props5.layout,
  582. outerElementType = _this$props5.outerElementType,
  583. outerTagName = _this$props5.outerTagName,
  584. style = _this$props5.style,
  585. useIsScrolling = _this$props5.useIsScrolling,
  586. width = _this$props5.width;
  587. var isScrolling = this.state.isScrolling; // TODO Deprecate direction "horizontal"
  588.  
  589. var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
  590. var onScroll = isHorizontal ? this._onScrollHorizontal : this._onScrollVertical;
  591. var _this$_getRangeToRend = this._getRangeToRender(),
  592. startIndex = _this$_getRangeToRend[0],
  593. stopIndex = _this$_getRangeToRend[1];
  594. var items = [];
  595. if (itemCount > 0) {
  596. for (var _index = startIndex; _index <= stopIndex; _index++) {
  597. items.push(React.createElement(children, {
  598. data: itemData,
  599. key: itemKey(_index, itemData),
  600. index: _index,
  601. isScrolling: useIsScrolling ? isScrolling : undefined,
  602. style: this._getItemStyle(_index)
  603. }));
  604. }
  605. } // Read this value AFTER items have been created,
  606. // So their actual sizes (if variable) are taken into consideration.
  607.  
  608. var estimatedTotalSize = getEstimatedTotalSize(this.props, this._instanceProps);
  609. return React.createElement(outerElementType || outerTagName || 'div', {
  610. className: className,
  611. onScroll: onScroll,
  612. ref: this._outerRefSetter,
  613. style: _extends({
  614. position: 'relative',
  615. height: height,
  616. width: width,
  617. overflow: 'auto',
  618. WebkitOverflowScrolling: 'touch',
  619. willChange: 'transform',
  620. direction: direction
  621. }, style)
  622. }, React.createElement(innerElementType || innerTagName || 'div', {
  623. children: items,
  624. ref: innerRef,
  625. style: {
  626. height: isHorizontal ? '100%' : estimatedTotalSize,
  627. pointerEvents: isScrolling ? 'none' : undefined,
  628. width: isHorizontal ? estimatedTotalSize : '100%'
  629. }
  630. }));
  631. };
  632. _proto._callPropsCallbacks = function _callPropsCallbacks() {
  633. if (typeof this.props.onItemsRendered === 'function') {
  634. var itemCount = this.props.itemCount;
  635. if (itemCount > 0) {
  636. var _this$_getRangeToRend2 = this._getRangeToRender(),
  637. _overscanStartIndex = _this$_getRangeToRend2[0],
  638. _overscanStopIndex = _this$_getRangeToRend2[1],
  639. _visibleStartIndex = _this$_getRangeToRend2[2],
  640. _visibleStopIndex = _this$_getRangeToRend2[3];
  641. this._callOnItemsRendered(_overscanStartIndex, _overscanStopIndex, _visibleStartIndex, _visibleStopIndex);
  642. }
  643. }
  644. if (typeof this.props.onScroll === 'function') {
  645. var _this$state2 = this.state,
  646. _scrollDirection = _this$state2.scrollDirection,
  647. _scrollOffset = _this$state2.scrollOffset,
  648. _scrollUpdateWasRequested = _this$state2.scrollUpdateWasRequested;
  649. this._callOnScroll(_scrollDirection, _scrollOffset, _scrollUpdateWasRequested);
  650. }
  651. } // Lazily create and cache item styles while scrolling,
  652. // So that pure component sCU will prevent re-renders.
  653. // We maintain this cache, and pass a style prop rather than index,
  654. // So that List can clear cached styles and force item re-render if necessary.
  655. ;
  656.  
  657. _proto._getRangeToRender = function _getRangeToRender() {
  658. var _this$props6 = this.props,
  659. itemCount = _this$props6.itemCount,
  660. overscanCount = _this$props6.overscanCount;
  661. var _this$state3 = this.state,
  662. isScrolling = _this$state3.isScrolling,
  663. scrollDirection = _this$state3.scrollDirection,
  664. scrollOffset = _this$state3.scrollOffset;
  665. if (itemCount === 0) {
  666. return [0, 0, 0, 0];
  667. }
  668. var startIndex = getStartIndexForOffset(this.props, scrollOffset, this._instanceProps);
  669. var stopIndex = getStopIndexForStartIndex(this.props, startIndex, scrollOffset, this._instanceProps); // Overscan by one item in each direction so that tab/focus works.
  670. // If there isn't at least one extra item, tab loops back around.
  671.  
  672. var overscanBackward = !isScrolling || scrollDirection === 'backward' ? Math.max(1, overscanCount) : 1;
  673. var overscanForward = !isScrolling || scrollDirection === 'forward' ? Math.max(1, overscanCount) : 1;
  674. return [Math.max(0, startIndex - overscanBackward), Math.max(0, Math.min(itemCount - 1, stopIndex + overscanForward)), startIndex, stopIndex];
  675. };
  676. return List;
  677. }(React.PureComponent), _class.defaultProps = {
  678. direction: 'ltr',
  679. itemData: undefined,
  680. layout: 'vertical',
  681. overscanCount: 2,
  682. useIsScrolling: false
  683. }, _class;
  684. } // NOTE: I considered further wrapping individual items with a pure ListItem component.
  685. // This would avoid ever calling the render function for the same index more than once,
  686. // But it would also add the overhead of a lot of components/fibers.
  687. // I assume people already do this (render function returning a class component),
  688. // So my doing it would just unnecessarily double the wrappers.
  689.  
  690. var validateSharedProps$1 = function validateSharedProps(_ref2, _ref3) {
  691. _ref2.children;
  692. _ref2.direction;
  693. _ref2.height;
  694. _ref2.layout;
  695. _ref2.innerTagName;
  696. _ref2.outerTagName;
  697. _ref2.width;
  698. _ref3.instance;
  699. };
  700. var FixedSizeList = /*#__PURE__*/createListComponent({
  701. getItemOffset: function getItemOffset(_ref, index) {
  702. var itemSize = _ref.itemSize;
  703. return index * itemSize;
  704. },
  705. getItemSize: function getItemSize(_ref2, index) {
  706. var itemSize = _ref2.itemSize;
  707. return itemSize;
  708. },
  709. getEstimatedTotalSize: function getEstimatedTotalSize(_ref3) {
  710. var itemCount = _ref3.itemCount,
  711. itemSize = _ref3.itemSize;
  712. return itemSize * itemCount;
  713. },
  714. getOffsetForIndexAndAlignment: function getOffsetForIndexAndAlignment(_ref4, index, align, scrollOffset, instanceProps, scrollbarSize) {
  715. var direction = _ref4.direction,
  716. height = _ref4.height,
  717. itemCount = _ref4.itemCount,
  718. itemSize = _ref4.itemSize,
  719. layout = _ref4.layout,
  720. width = _ref4.width;
  721. // TODO Deprecate direction "horizontal"
  722. var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
  723. var size = isHorizontal ? width : height;
  724. var lastItemOffset = Math.max(0, itemCount * itemSize - size);
  725. var maxOffset = Math.min(lastItemOffset, index * itemSize);
  726. var minOffset = Math.max(0, index * itemSize - size + itemSize + scrollbarSize);
  727. if (align === 'smart') {
  728. if (scrollOffset >= minOffset - size && scrollOffset <= maxOffset + size) {
  729. align = 'auto';
  730. } else {
  731. align = 'center';
  732. }
  733. }
  734. switch (align) {
  735. case 'start':
  736. return maxOffset;
  737. case 'end':
  738. return minOffset;
  739. case 'center':
  740. {
  741. // "Centered" offset is usually the average of the min and max.
  742. // But near the edges of the list, this doesn't hold true.
  743. var middleOffset = Math.round(minOffset + (maxOffset - minOffset) / 2);
  744. if (middleOffset < Math.ceil(size / 2)) {
  745. return 0; // near the beginning
  746. } else if (middleOffset > lastItemOffset + Math.floor(size / 2)) {
  747. return lastItemOffset; // near the end
  748. } else {
  749. return middleOffset;
  750. }
  751. }
  752. case 'auto':
  753. default:
  754. if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
  755. return scrollOffset;
  756. } else if (scrollOffset < minOffset) {
  757. return minOffset;
  758. } else {
  759. return maxOffset;
  760. }
  761. }
  762. },
  763. getStartIndexForOffset: function getStartIndexForOffset(_ref5, offset) {
  764. var itemCount = _ref5.itemCount,
  765. itemSize = _ref5.itemSize;
  766. return Math.max(0, Math.min(itemCount - 1, Math.floor(offset / itemSize)));
  767. },
  768. getStopIndexForStartIndex: function getStopIndexForStartIndex(_ref6, startIndex, scrollOffset) {
  769. var direction = _ref6.direction,
  770. height = _ref6.height,
  771. itemCount = _ref6.itemCount,
  772. itemSize = _ref6.itemSize,
  773. layout = _ref6.layout,
  774. width = _ref6.width;
  775. // TODO Deprecate direction "horizontal"
  776. var isHorizontal = direction === 'horizontal' || layout === 'horizontal';
  777. var offset = startIndex * itemSize;
  778. var size = isHorizontal ? width : height;
  779. var numVisibleItems = Math.ceil((size + scrollOffset - offset) / itemSize);
  780. return Math.max(0, Math.min(itemCount - 1, startIndex + numVisibleItems - 1 // -1 is because stop index is inclusive
  781. ));
  782. },
  783.  
  784. initInstanceProps: function initInstanceProps(props) {// Noop
  785. },
  786. shouldResetStyleCacheOnItemSizeChange: true,
  787. validateProps: function validateProps(_ref7) {
  788. _ref7.itemSize;
  789. }
  790. });
  791.  
  792. /* globals GM */
  793. function Explorer(root, hooks) {
  794. function runHooks(name, ...args) {
  795. if (!(name in hooks)) {
  796. return;
  797. }
  798. if (!Array.isArray(hooks[name])) {
  799. hooks[name] = [hooks[name]];
  800. }
  801. return Promise.all(hooks[name].map(f => f(...args)));
  802. }
  803. class AlbumListItem extends React__namespace.Component {
  804. constructor(props) {
  805. super(props);
  806. _defineProperty(this, "handleAlbumClick", ev => {
  807. const targetStyle = ev.target.style;
  808. targetStyle.cursor = document.body.style.cursor = 'wait';
  809. const url = this.state.TralbumData.url;
  810. window.setTimeout(function () {
  811. runHooks('playAlbumFromUrl', url).then(function () {
  812. targetStyle.cursor = document.body.style.cursor = '';
  813. });
  814. }, 1);
  815. });
  816. _defineProperty(this, "handleContextMenu", ev => {
  817. ev.preventDefault();
  818. ev.target.classList.add('selected');
  819. const TralbumData = this.state.TralbumData;
  820. const url = TralbumData.url;
  821. if (!confirm(`Delete album "${TralbumData.current.title}" by ${TralbumData.artist}?`)) {
  822. ev.target.classList.remove('selected');
  823. return;
  824. }
  825. window.setTimeout(() => {
  826. runHooks('deletePermanentTralbum', url).then(function () {
  827. ev.target.classList.remove('selected');
  828. ev.target.style.visibility = 'hidden';
  829. });
  830. }, 1);
  831. });
  832. this.state = {
  833. TralbumData: props.data.library[Object.keys(props.data.library)[props.index]]
  834. };
  835. }
  836. render() {
  837. return /*#__PURE__*/React__namespace.createElement("div", {
  838. className: `albumListItem ${this.props.index % 2 ? 'albumListItemOdd' : ''}`,
  839. onClick: this.handleAlbumClick,
  840. onContextMenu: this.handleContextMenu,
  841. title: "Click to play",
  842. style: this.props.style
  843. }, this.state.TralbumData.artist, " - ", this.state.TralbumData.current.title);
  844. }
  845. }
  846. class AlbumList extends React__namespace.Component {
  847. constructor(props) {
  848. super(props);
  849. this.state = {
  850. library: {},
  851. isLoading: false,
  852. error: null
  853. };
  854. if (!this.props.getKey) {
  855. throw Error('<AlbumList> needs a getKey property');
  856. }
  857. }
  858. componentDidMount() {
  859. this.setState({
  860. isLoading: true
  861. });
  862. GM.getValue(this.props.getKey, '{}').then(s => JSON.parse(s)).then(data => this.setState({
  863. library: data,
  864. isLoading: false
  865. })).catch(error => this.setState({
  866. error,
  867. isLoading: false
  868. }));
  869. }
  870. render() {
  871. const {
  872. library,
  873. isLoading,
  874. error
  875. } = this.state;
  876. if (error) {
  877. return /*#__PURE__*/React__namespace.createElement("p", null, error.message);
  878. }
  879. if (isLoading) {
  880. return /*#__PURE__*/React__namespace.createElement("p", null, "Loading ...");
  881. }
  882. return /*#__PURE__*/React__namespace.createElement(FixedSizeList, {
  883. className: "List",
  884. height: 600,
  885. itemCount: Object.keys(library).length,
  886. itemSize: 35
  887. //width={600}
  888. ,
  889. itemData: {
  890. library: library
  891. }
  892. }, AlbumListItem);
  893. }
  894. }
  895. this.render = function () {
  896. ReactDOM__namespace.createRoot(root).render( /*#__PURE__*/React__namespace.createElement(AlbumList, {
  897. getKey: "tralbumlibrary"
  898. }));
  899. };
  900. }
  901.  
  902. var discographyplayerCSS = ".cll{clear:left}.clb{clear:both}#discographyplayer{z-index:1010;position:fixed;bottom:0;height:83px;width:100%;padding-top:3px;background:#fff;color:#505958;border-top:1px solid rgba(0,0,0,.15);font:13px/1.231 \"Helvetica Neue\",Helvetica,Arial,sans-serif;transition:bottom .5s}#discographyplayer a:link,#discographyplayer a:visited{color:#0687f5;text-decoration:none;cursor:pointer}#discographyplayer a:hover{color:#0687f5;text-decoration:underline;cursor:pointer}#discographyplayer .nowPlaying .cover,#discographyplayer .nowPlaying .info{display:inline-block;vertical-align:top}#discographyplayer .nowPlaying img{width:60px;height:60px;margin-top:4px;margin-left:4px;margin-bottom:4px}#discographyplayer .nowPlaying .info{line-height:18px;margin-left:8px;margin-top:8px;max-width:calc(100% - 76px);border:0 solid #000;padding:0;width:auto;max-height:auto;overflow-y:hidden}#discographyplayer .nowPlaying .info .album,#discographyplayer .nowPlaying .info .title{font-size:13px;font-weight:400;color:#0687f5;margin:0;padding:0}#discographyplayer .currentlyPlaying{display:inline-block;vertical-align:top;overflow:hidden;transition:margin-left 3s ease-in-out;width:99%}#discographyplayer .nextInRow{display:inline-block;vertical-align:top;width:0%;overflow:hidden;transition:width 6s ease-in-out}#discographyplayer .durationDisplay{margin-top:24px;float:left}#discographyplayer .downloadlink:link{display:block;float:right;margin-top:22px;font-size:15px;padding:0 3px;color:#0687f5;border:1px solid #0687f5;transition:color .3s ease-in-out,border-color .3s ease-in-out}#discographyplayer .downloadlink:hover{text-decoration:none;background-color:#0687f5;color:#fff;border:1px solid #fff}#discographyplayer .downloadlink.downloading{color:#f0f;border-color:#f0f;animation:downloadrotation 3s infinite linear;cursor:wait}@keyframes downloadrotation{from{transform:rotate(0)}to{transform:rotate(359deg)}}#discographyplayer .controls{margin-top:10px;width:auto;float:left}#discographyplayer .controls>*{display:inline-block;cursor:pointer;border:1px solid #d9d9d9;padding:11px;margin-right:4px;height:18px;width:17px;transition:background-color .1s}#discographyplayer .controls>:hover{background-color:#0687f52b}#discographyplayer .playpause .play{width:0;height:0;border-top:9px inset transparent;border-bottom:9px inset transparent;border-left:15px solid #222;cursor:pointer;margin-left:2px}#discographyplayer .playpause .pause{border:0;border-left:5px solid #2d2d2d;border-right:5px solid #2d2d2d;height:18px;width:4px;margin-right:2px;margin-left:1px}#discographyplayer .playpause .busy{background-image:url(https://bandcamp.com/img/playerbusy-noborder.gif);background-position:50% 50%;background-repeat:no-repeat;border:none;height:30px;margin:0 0 0 -3px;width:25px;overflow:hidden;background-size:contain}#discographyplayer .shuffleswitch .shufflebutton{background-size:cover;background-position-y:0px;filter:drop-shadow(#FFFF 0px 0px 0px);transition:filter .5s;border:0;height:13px;width:20px;margin-top:4px}#discographyplayer .shuffleswitch .shufflebutton.active{filter:drop-shadow(#0060F2 1px 1px 2px)}#discographyplayer .arrowbutton{border:0;height:13px;width:20px;margin-top:4px;background:url(https://bandcamp.com/img/nextprev.png) 0 0/40px 12px no-repeat transparent;background-position-x:0px;cursor:pointer}#discographyplayer .arrowbutton.next-icon{background-position:100% 0}#discographyplayer .arrowbutton.prevalbum-icon{border-right:3px solid #2d2d2d}#discographyplayer .arrowbutton.nextalbum-icon{background-position:100% 0;border-left:3px solid #2d2d2d}#timeline{width:100%;background:rgba(50,50,50,.4);margin-top:5px;border-left:1px solid #000;border-right:1px solid #000}#playhead{width:10px;height:10px;border-radius:50%;background:#323232;cursor:pointer}.bufferbaranimation{transition:width 1s}#bufferbar{position:absolute;width:0;height:10px;background:rgba(0,0,0,.1)}#discographyplayer .playlist{position:relative;width:100%;display:inline-block;max-height:80px;overflow:auto;list-style:none;margin:0;padding:0 5px 0 5px;scrollbar-color:rgba(50,50,50,0.4) white;background:#fff}#discographyplayer_contextmenu{position:absolute;box-shadow:#000000b0 2px 2px 2px;background-color:#fff;border:#619aa9 2px solid;z-index:1011}#discographyplayer_contextmenu .contextmenu_submenu{cursor:pointer;padding:2px;border:1px solid #619aa9}#discographyplayer_contextmenu .contextmenu_submenu:hover{background-color:#619aa9;color:#fff;border:1px solid #fff}#discographyplayer .playlist .isselected{border:1px solid red}#discographyplayer .playlist .playlistentry{cursor:pointer;margin:1px 0}#discographyplayer .playlist .playlistentry .duration{float:right}#discographyplayer .playlist .playing{background:#619aa950}#discographyplayer .playlist .playlistheading{background:rgba(50,50,50,.4);margin:3px 0}#discographyplayer .playlist .playlistheading a:hover,#discographyplayer .playlist .playlistheading a:link,#discographyplayer .playlist .playlistheading a:visited{color:#eee;cursor:pointer}#discographyplayer .playlist .playlistheading a.notloaded{color:#ccc}#discographyplayer .playlist .playlistheading.notloaded{cursor:copy}#discographyplayer .vol{float:left;position:relative;width:100px;margin-left:1em;margin-top:1em}#discographyplayer .vol-icon-wrapper{font-size:20px;cursor:pointer;width:27px}#discographyplayer .vol-slider{width:60px;height:10px;position:relative;cursor:pointer}#discographyplayer .vol>*{display:inline-block;vertical-align:middle}#discographyplayer .vol-bg{background:rgba(50,50,50,.4);width:100%;margin-top:4px;height:3px;position:absolute}#discographyplayer .vol-amt{margin-top:4px;height:3px;position:absolute;background:#323232}#discographyplayer .vol-control-outer{height:100%;position:relative;margin-left:-3px;margin-right:5px}#discographyplayer .collect{float:left;margin-left:1em}#discographyplayer .{cursor:default;margin-top:.5em}#discographyplayer .collect-wishlist .wishlist-add{cursor:pointer}#discographyplayer .collect-listened{cursor:pointer;margin-top:.5em;margin-left:2px}#discographyplayer .collect .icon{height:13px;width:14px;display:inline-block;position:relative;top:2px}#discographyplayer .collect .add-item-icon{background-position:0 -73px}#discographyplayer .collect .collected-item-icon{background-position:-28px -73px}#discographyplayer .collect .own-item-icon{background-position:-42px -73px}#discographyplayer .collect .wishlist-add,#discographyplayer .collect .wishlist-collected,#discographyplayer .collect .wishlist-own,#discographyplayer .collect .wishlist-saving{display:none}#discographyplayer .collect .wishlist-add:hover .add-item-icon{background-position:-56px -73px}#discographyplayer .collect .wishlist-add .add-item-label:hover{text-decoration:underline}#discographyplayer .collect .listened,#discographyplayer .collect .listened-saving,#discographyplayer .collect .mark-listened{display:none}#discographyplayer .collect .listened .listened-symbol{color:#00dc32;text-shadow:1px 0 #ddd,-1px 0 #ddd,0 -1px #ddd,0 1px #ddd}#discographyplayer .collect .mark-listened .mark-listened-symbol{color:#fff;text-shadow:1px 0 #959595,-1px 0 #959595,0 -1px #959595,0 1px #959595}#discographyplayer .collect .mark-listened:hover .mark-listened-symbol{text-shadow:1px 0 #0af,-1px 0 #0af,0 -1px #0af,0 1px #0af}#discographyplayer .collect .mark-listened:hover .mark-listened-label{text-decoration:underline}#discographyplayer .closebutton,#discographyplayer .minimizebutton{position:absolute;top:1px;right:1px;border:1px solid #505958;color:#505958;font-size:10px;box-shadow:0 0 2px #505958;cursor:pointer;opacity:0;transition:opacity .3s;min-width:8px;min-height:13px;text-align:center}#discographyplayer .minimizebutton{right:13px}#discographyplayer .minimizebutton .minimized{display:none}#discographyplayer .minimizebutton.minimized .maximized{display:none}#discographyplayer .minimizebutton.minimized .minimized{display:inline}#discographyplayer:hover .closebutton,#discographyplayer:hover .minimizebutton{opacity:1}#discographyplayer .col{float:left;min-height:1px;position:relative}#discographyplayer .col25{width:25%}#discographyplayer .col35{width:35%}#discographyplayer .col30{width:30%}#discographyplayer .col15{width:14%}#discographyplayer .col20{width:20%}#discographyplayer .colcontrols{user-select:none}#discographyplayer .colvolumecontrols{margin-left:10px}.albumIsCurrentlyPlaying{border:2px solid #0f0}.albumIsCurrentlyPlaying+.art-play{display:none}.dig-deeper-item .albumIsCurrentlyPlaying,.music-grid-item .albumIsCurrentlyPlaying{border:none}.albumIsCurrentlyPlayingIndicator{display:none}.dig-deeper-item .albumIsCurrentlyPlayingIndicator,.music-grid-item .albumIsCurrentlyPlayingIndicator{position:absolute;display:block;width:74px;height:54px;left:50%;top:50%;margin-left:-36px;margin-top:-27px;opacity:.5;transition:opacity .2s}.albumIsCurrentlyPlayingIndicator .currentlyPlayingBg{position:absolute;width:100%;height:100%;left:0;top:0;background:#000;border-radius:4px}.albumIsCurrentlyPlayingIndicator .currentlyPlayingIcon{position:absolute;width:10px;height:20px;left:28px;top:17px;border-width:0 5px;border-color:#fff;border-style:solid}@media (max-width:1600px){#discographyplayer .controls>*{padding:4px 11px 5px 11px;height:18px}#discographyplayer .durationDisplay{margin-top:0}#discographyplayer .downloadlink:link{margin-top:0}}@media (max-width:1170px){#discographyplayer .colcontrols{width:39%}#discographyplayer .colvolumecontrols{display:none}}";
  903.  
  904. var discographyplayerSidebarCSS = "@media (min-width:1600px){#menubar-wrapper:hover{z-index:1100}#discographyplayer{display:block;bottom:0;height:100vh;max-height:100vh;width:calc((100vw - 915px - 35px)/ 2);right:0;border-left:1px solid #0007;padding-left:1px}#discographyplayer .playlist{height:calc(100vh - 80px - 80px - 50px - 13px);max-height:calc(100vh - 80px - 80px - 50px - 13px)}#discographyplayer .playlist .playlistentry{overflow-x:hidden}#discographyplayer .col25{width:98%}#discographyplayer .col.nowPlaying{height:70px}#discographyplayer .col.col25.colcontrols{height:85px}#discographyplayer .col35{width:97%}#discographyplayer .col15{width:96%}#discographyplayer .colvolumecontrols{height:50px}#bufferbar,#playhead{height:25px;border-radius:0}#discographyplayer .audioplayer a.downloadlink{position:fixed;bottom:5px;right:5px;z-index:10}#discographyplayer .minimizebutton{display:none}#discographyplayer .currentlyPlaying{transition:margin-top 1s ease-in-out;width:99%;height:99%}#discographyplayer .nextInRow{height:0%;width:99%;transition:height 1s ease-in-out}}";
  905.  
  906. var pastreleasesCSS = "#pastreleases{position:fixed;bottom:1%;left:10px;background:#d5dce4;color:#033162;font-size:10pt;border:1px solid #033162;z-index:200;opacity:0;transition:opacity .7s;overflow:auto}#pastreleases .tablediv{display:table;position:relative}#pastreleases .entry,#pastreleases .header{display:table-row}#pastreleases .entry>*,#pastreleases .header>*{display:table-cell;line-height:21pt}#pastreleases .upcoming{cursor:pointer;font-size:x-small}#pastreleases .controls{cursor:pointer;position:absolute;top:0;right:1px;line-height:11pt}#pastreleases .entry:link{position:relative;border-top:1px solid #033162;color:#033162;text-decoration:none}#pastreleases .entry:nth-child(odd){background:#c5ccd4}#pastreleases .entry:hover,#pastreleases .entry:visited{color:#033162;text-decoration:none}#pastreleases .entry.future{display:none;background:#9fc2ea}#pastreleases .entry.future:nth-child(odd){background:#8fc2e1}#pastreleases .entry .image{background-size:contain;width:21pt;height:21pt}#pastreleases .entry:hover .image{display:block;position:fixed;bottom:10px;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);width:350px;height:350px;background:#000;border:5px solid #fff}#pastreleases .entry time{padding-right:2px}#pastreleases .entry .title{padding-left:2px;border-left:1px solid #47a2bd;font-size:1em}#pastreleases .remove{font-family:sans-serif;color:#97174e;font-size:small;padding-right:3px}";
  907.  
  908. var darkmodeCSS = "#centerWrapper #pgBd #trackInfoInner{display:flex;flex-direction:column}#centerWrapper #pgBd #trackInfoInner>.tralbumCommands{order:1}#centerWrapper #pgBd #rightColumn{display:flex;flex-direction:column}#centerWrapper #pgBd #rightColumn>#showography{order:1}.ui-widget-overlay{display:none}.ui-dialog.ui-widget.ui-widget-content.ui-corner-all.nu-dialog.no-title{position:fixed!important;top:0!important;right:0!important;bottom:auto!important;left:auto!important}.inline_player .nextbutton,.inline_player .prevbutton,svg{filter:invert(90%)}a{color:#da5!important}.trackYear,button{color:#ac6!important}div#collection-container.collection-container,div.home{background:#000!important}div.area_text,div.sort_controls,div.text,span{color:#ccc!important}div#dlg0_h.hd,div#pgBd.yui-skin-sam,div.blogunit-details-section,div.collection-item-details-container{background:var(--pgBdColor)!important}div.collection-item-artist,h1{color:#ccc!important}DIV.track_number.secondaryText,div.collection-item-title,div.message,h2{color:#fff!important}h3{color:#ffed80!important}DIV.tralbumData.tralbum-credits{color:#ccc!important}DIV#license.info,DIV.tralbumData.tralbum-about,DIV.tralbumData.tralbum-feed,li{color:#806300!important}button.sc-button.sc-button-small.sc-button-responsive.sc-button-addtoset{color:#000!important}div#fan-suggestions.dotted-section.mine,div.bcweekly-bd,div.collection-item-gallery-container,div.collection-stats.dotted-section.mine{background:#222!important}p{color:#aaa!important}div.sound__soundActions{background:0 0!important}button.sc-button.sc-button-small.sc-button-responsive.sc-button-addtoset{color:#111!important}div.ft.fakeFt{background:#555!important}div.bd.footerless{background:#999!important}.walkthrough ol{background-color:#373737}.walkthrough .button{background:#262626;border:#262626}.fan-banner.empty.owner{background-color:#373737}#menubar,#pgFt,.menubar-outer{background-color:#26423b!important;border-bottom:dotted #000 1px!important}#menubar-wrapper{background-color:#000;border-bottom:dotted #000 1px!important}#menubar input#search-field{margin:0;height:21px;line-height:21px;width:222px;font-family:\"Helvetica Neue\",Arial,sans-serif;color:#fff;font-size:13px;padding:0 21px 0 3px;-webkit-user-select:text;text-align:center;background-color:#282828;border:1px solid #282828;outline:0;border-radius:3px}#menubar input#search-field.focused{background-color:#282828;border:1px solid #282828}#menubar.menubar-2018 .hoverable:hover{background:#11607582!important}.fan-bio .edit-profile a{border:1px solid #373737;border-radius:5px;outline:0;background:#373737;color:#aaa;font-weight:500;padding:5px 9px;font-size:11px;line-height:15px;text-transform:uppercase;display:inline-block}.grids{color:#fff;margin:0 0 100px}.recommendations-container{background-color:#373737;border-top:dotted #373737 1px}.fan-container .top.editing{border-bottom:1px solid #2a2a2a;background-color:#191919}.ui-dialog.nu-dialog .ui-dialog-titlebar{padding:15px 20px 12px;background-color:#26423b!important;border-bottom:1px solid #26423b!important}.ui-dialog-titlebar *{color:#fff!important}.ui-dialog-content{color:#ddd!important}.ui-widget-content{border:1px solid #373;background:#373737!important;color:#ddd!important}.external-follow-confirm .ui-dialog-buttonset button,.mailing-list-opt-in .ui-dialog-buttonset button{background:#26423b!important}.external-follow-confirm .ui-dialog-buttonset button:last-child,.mailing-list-opt-in.band .ui-dialog-buttonset button:last-child{background:#0002!important;border:2px solid #26423b!important}#follow-unfollow{background:0 0!important}#follow-unfollow.following{background:#26423b!important;border-color:#26423b!important}#follow-unfollow>div{color:#ac6!important}#follow-unfollow.following>div{background:#26423b!important}.app-promo-desktop,.bcdaily,.discover,.email-intake,.notable{background-color:#262626}.bcdaily .bcdaily-story{min-height:280px;background:#373737}.notable-item{background-color:#373737}.item-page{background:#373737;border:1px solid #373737}.follow-fan-btn{background-color:#373737;border:1px solid #373737}.spotlight-bio,.spotlight-button,.spotlight-link,.spotlight-location,.spotlight-name{color:#fff}.aotd-large{background:#373737}.factoid-title{color:#46c5d5}#autocomplete-results.autocompleted{background:#262626;border:1px solid #262626;color:#fff}.searchwidget.keyboard-focus input[type=text]:focus{background:#262626;box-shadow:0 0}.discover-detail-inner{background-color:#373737}body.wordpress{background:#262626}.wordpress .sidebar .textwidget{color:#fff}.wordpress h1 a{display:block;height:60px;background-size:242px 28px;background-position:24.6% 50%}p{color:#fff!important}.wordpress #content{color:#fff}#dash-container .follow-band,#dash-container .follow-discover,#dash-container .follow-fan{border:1px solid #373737;background:linear-gradient(to bottom,#373737 0,#373737 100%)}html{background:#1e1e1e!important}#stories-vm .story-innards{background-color:#373737}.pane{color:#c7c7c7}#settings-menubar{border-right:1px solid #383838}#settings-menubar li{border-left:1px solid #383838;border-bottom:1px solid #383838;border-top:1px solid #383838}.share_dialog.ui-dialog .ui-dialog-content{background-color:#262626}.share_dialog .section_head{color:#fff}.buy-dlg{color:#fff}.pg-ft{background-color:#000}#lang-picker-vm{border-radius:10px}#menubar>ul>li .logo{background:url('https://www.dropbox.com/s/8s7km8r329l7qy7/bandcamp-logo-gray.png?dl=1') 0 0 no-repeat;background-size:contain;height:20px;margin-top:15px;width:85px}.hd-logo{background:transparent url('https://www.dropbox.com/s/8s7km8r329l7qy7/bandcamp-logo-gray.png?dl=1') no-repeat;background-size:100%;margin-top:24px;height:25px;width:156px}.wordpress h1 a{display:block;text-indent:-999em;background:url('https://www.dropbox.com/s/mx80o2eenp43l0o/bandcamp-daily-retina-dark-theme.png?dl=1') no-repeat;height:60px;background-size:242px 28px;background-position:24.6% 50%}#pgBd{color:#fff}.download-bottom-area{border-top:none;background:0 0}.download .formats-container{border:1px solid #373737;background-color:#373737}.download .formats{list-style:none;color:#888;padding:0;background-color:#373737;width:170px;z-index:2;cursor:default}.download .formats li:hover{background-color:#262626}html{scrollbar-color:#222 #26423b}::-webkit-scrollbar{height:13px}::-webkit-scrollbar-thumb{background:#26423b;border:1px solid #4a4a4a}::-webkit-scrollbar-thumb:hover{background:#316d4b}::-webkit-scrollbar-thumb:active{background:#316d4b}::-webkit-scrollbar-track{background:#4a4a4a}::-webkit-scrollbar-track:hover{background:#4a4a4a}::-webkit-scrollbar-track:active{background:#4a4a4a}::-webkit-scrollbar-corner{background:#4a4a4a}body{background-color:#000!important;color:#fff!important}#propOpenWrapper{background-color:var(--propOpenWrapperBackgroundColor)!important;transition:background-color .5s}.bcdaily-thumb-img,img{filter:brightness(70%)}.bcdaily-thumb-img:hover,img:hover{filter:none}img.imageviewer_image{filter:none}.bclogo svg{filter:brightness(60%)}.inline_player .playbutton.busy::after{opacity:.3;background-image:url('https://bandcamp.com/img/loading-dark.gif')}.inline_player .nextsongcontrolbutton,.inline_player .playbutton,.inline_player .volumeButton,.track_list .play_status{background-color:#686868;border-color:#595959}.nextsongcontrolbutton .nextsongcontrolicon{filter:drop-shadow(#090909b3 1px 1px 2px)}.nextsongcontrolbutton.active .nextsongcontrolicon{filter:drop-shadow(#a3f204 1px 1px 2px)!important}.hidden .nextsongcontrolbutton{display:none}.inline_player .progbar .thumb{background-color:#000;border-color:#ccc}.inline_player .nextbutton,.inline_player .prevbutton{opacity:.7}.track_list tr.lyricsRow td[colspan] div{color:#f8f8f8}input[type=password],input[type=text],textarea{background-color:#121f12!important;color:#40b333!important}.carousel-player-inner{background-color:#26423b}.carousel-player-inner .progress-bar{background-color:#26423b}#carousel-player .queue.show{background-color:#26423b}#carousel-player .queue.show li.active{background-color:#528679}#autocomplete-results .see-all{background-color:#f3f3f345!important}.deluxemenu{color:#c9ebfb!important;background:#00042f!important}.deluxemenu button{background:#1c1494}.deluxeexportmenu table tr>td{color:#00a1c6!important}.deluxeexportmenu table tr>td:nth-child(3){color:#006bc6!important}.deluxemenu fieldset{border:1px solid #fffa!important;box-shadow:1px 1px 3px #fff5!important}.deluxemenu fieldset legend{color:#fffa!important}#discographyplayer{background-color:#26423b!important;color:#869593!important}#discographyplayer .playlist{background:#26423b!important}#discographyplayer .playlist .playing{background:#619aa9db!important}#timeline{background:rgba(34,57,42,.69)!important}#bufferbar{background:rgba(77,79,76,.59)!important}#playhead{background:#2a6c21!important}#discographyplayer .playlist{scrollbar-color:#222 #26423b!important}#discographyplayer_contextmenu{box-shadow:#ffffff50 2px 2px 2px;background-color:#162d27;border:#619aa9 2px solid;color:#c2aa4a}#discographyplayer_contextmenu .contextmenu_submenu{cursor:pointer;padding:2px;background-color:#162d27;color:#c2aa4a;border:1px solid transparent}#discographyplayer_contextmenu .contextmenu_submenu:hover{background-color:#619aa9;color:#fff;border:1px solid #fff}#band-navbar{background-color:#333!important}.hd.corp-home{background-color:#26423b}#hub .bd-section.top-section{opacity:.8}#s-daily{background:#262626!important}.franchise-description{color:#d7d072}.footer-gradient{background-image:linear-gradient(to bottom,#262626,#5e5e5e)}#s-daily dailyfooter{background-color:#5e5e5e}#s-daily dailyfooter h2{-webkit-text-stroke:2px #257110!important}#s-daily a.pagination-link{-webkit-text-stroke:2px #257110!important}#s-daily a.pagination-link .back-text{-webkit-text-stroke:2px #1c6c3f!important}article-title{color:#e3e3e3}.mpmerchformats{color:#909090}article-footer{color:#909090}article>article-end{filter:invert(75%)}article .icon{filter:invert(50%)}.salesfeed .item-inner:hover{background-color:#0e738c!important}.hd.header-rework-2018 .hd-sub-head .blue-gradient{background:-webkit-linear-gradient(left,#da5,#daf)!important}.factoid .dots{filter:brightness(300%)}.bdp_check_onlinkhover_container_shown{background-color:#26423ba8!important}.bdp_check_onlinkhover_container:hover{background-color:#2d7d39a8!important;box-shadow:#2db91f7a 0 0 5px}#pastreleases{background-color:#154a86!important}#pastreleases .entry:nth-child(odd){background-color:#3e6c9f!important}#pastreleases .entry.future{background-color:#4783c8!important}#pastreleases .entry.future:nth-child(odd){background-color:#11447d!important}#queueloadingindicator{background-color:#154a86!important}.sidebar .shortcuts{background:#0000;border-color:#0000}";
  909.  
  910. var geniusCSS = "#myconfigwin39457845{z-index:2060!important;position:fixed!important}#myconfigwin39457845 h1{margin:5px}#myconfigwin39457845 .divAutoShow{display:none}#myconfigwin39457845 button{background-color:#cacaca!important;color:#000!important;border:2px outset!important;padding:1px!important;font-size:1.2em!important}#lyricsiframe{opacity:.1;transition:opacity 2s;margin:0;padding:0;position:relative}.lyricsnavbar{font-size:.7em;text-align:right;padding:0 10px 0 0!important}.lyricsnavbar a:link,.lyricsnavbar a:visited,.lyricsnavbar span{color:#606060;text-decoration:none;transition:color .4s}.lyricsnavbar a:hover,.lyricsnavbar span:hover{color:#9026e0;text-decoration:none}.loadingspinner{color:#000;font-size:12px;line-height:15px;width:15px!important;height:15px!important;padding:2px!important}.loadingspinnerholder{z-index:10;cursor:progress;position:relative;width:20px!important;height:20px!important}.searchresultlist{margin:0!important;padding:0!important;border:1px solid #000;border-radius:3px;width:450px!important}.searchresultlist ol{list-style:none;padding:0!important;margin:0}.searchresultlist ol li div{width:auto!important}";
  911.  
  912. var exportMenuHTML = "<h2>Export played albums</h2>\n <h1 class=\"drophint\">Drop to restore from backup</h1>\n Available fields per album:<br>\n <table>\n <tr>\n <td>%artist%</td>\n <td>Artist name</td>\n <td>Jay-X</td>\n </tr>\n <tr>\n <td>%title%</td>\n <td>Song title</td>\n <td>Classic song</td>\n </tr>\n <tr>\n <td>%cover%</td>\n <td>Cover image url</td>\n <td>https://f4.bcbits.com/img/a2588527047_2.jpg</td>\n </tr>\n <tr>\n <td>%url%</td>\n <td>Album url</td>\n <td>petrolgirls.bandcamp.com/album/cut-stitch</td>\n </tr>\n <tr>\n <td>%releaseDate% / %releaseUnix% / %releaseTimestamp%</td>\n <td>Release date</td>\n <td>2019-02-07T14:01:59.100Z / 1549548119 / 1549548119100</td>\n </tr>\n <tr>\n <td>%listenedDate% / %listenedUnix% / %listenedTimestamp%</td>\n <td>Played/Listened date</td>\n <td>2019-02-07T02:17:21.315Z / 1549505841 / 1549505841315</td>\n </tr>\n <tr>\n <td>%releaseY% / %releaseYYYY%</td>\n <td>Release: Year</td>\n <td>19 / 2019</td>\n </tr>\n <tr>\n <td>%releaseM% / %releaseMM% / %releaseMon% / %releaseMonth%</td>\n <td>Release: Month</td>\n <td>2 / 02 / Feb / February</td>\n </tr>\n <tr>\n <td>%releaseD% / %releaseDD%</td>\n <td>Release: Day of month</td>\n <td>7 / 07</td>\n </tr>\n <tr>\n <td>%releaseDay%</td>\n <td>Release: Day of week</td>\n <td>Friday</td>\n </tr>\n <tr>\n <td>%listenedY% / %listenedYYYY%</td>\n <td>Played: Year</td>\n <td>19 / 2019</td>\n </tr>\n <tr>\n <td>%listenedM% / %listenedMM% / %listenedMon% / %listenedMonth%</td>\n <td>Played: Month</td>\n <td>2 / 02 / Feb / February</td>\n </tr>\n <tr>\n <td>%listenedD% / %listenedDD%</td>\n <td>Played: Day of month</td>\n <td>7 / 07</td>\n </tr>\n <tr>\n <td>%listenedDay%</td>\n <td>Played: Day of week</td>\n <td>Friday</td>\n </tr>\n\n </table>\n";
  913.  
  914. var speakerIconMuteSrc = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAoAQMAAACCSesyAAAABlBMVEUAAAA1NTVzRZghAAAAAXRSTlMAQObYZgAAAK1JREFUGNMtzzEOwjAMBdAgJMKWlYlcpGqvxVC1zgl6A3qRSmXrNYo6dE3FQCRCzXeCl+cvefBXB1Iyx0fiMOukNyTcKpJcVCT5asngzHRkZqX0RKtHWtwL2M19gmIO7ivEIkawl43AtqmFrmqEaUwsfSlsmZAZbOKe6f90jTBOCX5mfC3sITHEQnD7RbWAz/iM3RvvaqZ1RjMm49EFBNCSicCSLgHaWaCxAczpB9BXgdGWyYXIAAAAAElFTkSuQmCC";
  915.  
  916. var speakerIconLowSrc = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAoCAMAAACPWYlDAAAAM1BMVEUAAABqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampPcCe2AAAAEHRSTlMAN4Xs4SoS0bxHeJEgpm5gLbFq2AAAALlJREFUOMvF1MsKwzAMRNGxKz+bx/z/1xYl0EJIQLPqXUSLcBAmOLivFCiNRmbEy/QqgtXOo4RYxSiBjZTASgksnRIoRg1MRsB8feMFpIR695UeSp1sS4mD4Y9WhQ1vf74FgEMUAaD7CgUMkk0B1WcVAI5DqBuScgYVrD6XOCg+DHHQfcw4yOeCMNhPFgfHi025D5vZhAJw38i/HsBzWQXYVYDURIC6igCYKsAwXi5O6J9sUMrWEv7VB3zHKzcAIgoLAAAAAElFTkSuQmCC";
  917.  
  918. var speakerIconMiddleSrc = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAoCAMAAACPWYlDAAAANlBMVEUAAABqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqamrDZ907AAAAEXRSTlMANoQS7CRH3nPQtpDAnFSpYGW9KtUAAAEQSURBVDjLxZTbloMwCEUhhNy8lf//2elAx+XKNJU8db9EXW6JBxTegwwz7FUkgJ8gTyKBE1pFQfCBWaaEIjIlbNILjARDuEkvFJGYeHR/ll5gDQx5GGcvJD3MdDFCPJFOQCSyixvR4LFXoYlU3l8nfC/obipZzg0cFRZ5soA1nulesKYw6lnxCNC0RLU9OQQNNf8NLzkE+l3J9uQSQNNSTdhdoZiAHiGZ4K9w6Op/BxRNabHFIay6I5u/w9EHy/81TDvdCg+xULMOoWP4gs1eswIOAUuOgYKcBTyNA4s08kVI4WT4TScYEP4JmukGQx6xEwBrXOADWC+CCzomBKPMCpDipAC86u8R/FDIFeFb/AD0fTaBQdge8wAAAABJRU5ErkJggg==";
  919.  
  920. var speakerIconHighSrc = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAoCAMAAACPWYlDAAAAOVBMVEUAAABqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampqampHCtmUAAAAEnRSTlMAhTXgE+5yutBAH0yQKqibV2MOLOh8AAABXElEQVQ4y8WUW5aEIAxEeb/UVrP/xc5Mimk9IGn96vrhiLmkCBB1rWVRTzQlIv0gfqZfeXeeKkK4i8Qyx1S2ZLdRvLHUATw1XccHog4oxB4x0WilFijZIQMl14WXSC0QiPw0YWbuim+pBRaY2etU578DsLYtsPriKP8WNYDJqnhEOiT/O39NA+VIlMpWPzBqCZhQGfiMKrE3CTAzKoPKFYBGAhQTS+avUDCIgIqcIp08rTIwsW0N9y9wIuDYPTw5DkwyoLhaDkcQkOhzhlCB/QaQT0C5kQH7zOb2HhasOWOIn6sUcVQeF9Xi4AUA9a+XaTMYBGDHFcTKqcYVAdDnuxf+L4hkKVir62+rAjgRwJuGMePf3TDrQ6M3HWCs77e6A/gtR6epJmi1+wZQOfmVNzBoliY1AKfxl30Mcq8LoPaBgUIHqIjOOlI+mlaVm9PaxPc92aon0jZl9S39AOlqRk93STxjAAAAAElFTkSuQmCC";
  921.  
  922. /* globals GM, GM_addStyle, GM_download, GM_setClipboard, unsafeWindow, MouseEvent, JSON5, MediaMetadata, Response, geniusLyrics, Blob */
  923.  
  924. // TODO Mark as played automatically when played
  925. // TODO custom CSS
  926.  
  927. const BACKUP_REMINDER_DAYS = 35;
  928. const TRALBUM_CACHE_HOURS = 2;
  929. let NOTIFICATION_TIMEOUT = 3000;
  930. const CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
  931. const CAMPEXPLORER = document.location.hostname === 'campexplorer.io';
  932. const BANDCAMPDOMAIN = document.location.hostname === 'bandcamp.com' || document.location.hostname.endsWith('.bandcamp.com');
  933. let BANDCAMP = BANDCAMPDOMAIN;
  934. const NOEMOJI = CHROME && navigator.userAgent.match(/Windows (NT)? [4-9]/i);
  935. const DEFAULTSKIPTIME = 10; /* Seek time to skip in seconds by default */
  936. const SCRIPT_NAME = 'Bandcamp script (Deluxe Edition)';
  937. const LYRICS_EMPTY_PATH = '/robots.txt';
  938. const PLAYER_URL = 'https://bandcamp.com/robots.txt?player';
  939. const ONEHOUR = 3600000;
  940. let darkModeInjected = false;
  941. let storeTralbumDataPermanentlySwitch = true;
  942. const allFeatures = {
  943. discographyplayer: {
  944. name: 'Enable player on discography page',
  945. default: true
  946. },
  947. tagSearchPlayer: {
  948. name: 'Enable custom player on tag search page',
  949. default: true
  950. },
  951. albumPageVolumeBar: {
  952. name: 'Enable volume slider/shuffle/repeat on album page',
  953. default: true
  954. },
  955. albumPageAutoRepeatAll: {
  956. name: 'Always "repeat all" on album page',
  957. default: false
  958. },
  959. albumPageLyrics: {
  960. name: 'Show lyrics from genius.com on album page',
  961. default: true
  962. },
  963. markasplayed: {
  964. name: 'Show "mark as played" link on discography player',
  965. default: true
  966. },
  967. markasplayedEverywhere: {
  968. name: 'Show "mark as played" link everywhere',
  969. default: true
  970. },
  971. /* markasplayedAuto: {
  972. name: '(NOT YET IMPLEMENTED) Automatically "mark as played" once a song was played for',
  973. default: false
  974. }, */
  975. thetimehascome: {
  976. name: 'Circumvent "The time has come to open thy wallet" limit',
  977. default: true
  978. },
  979. albumPageDownloadLinks: {
  980. name: 'Show download links on album page',
  981. default: true
  982. },
  983. discographyplayerDownloadLink: {
  984. name: 'Show download link on discography player',
  985. default: true
  986. },
  987. discographyplayerSidebar: {
  988. name: 'Show discography player as a sidebar on the right',
  989. default: false
  990. },
  991. discographyplayerFullHeightPlaylist: {
  992. name: 'Extend discography player playlist to full screen height on mouse over',
  993. default: true
  994. },
  995. discographyplayerPersist: {
  996. name: 'Recover discography player on next page',
  997. default: true
  998. },
  999. backupReminder: {
  1000. name: 'Remind me to backup my played albums every month',
  1001. default: true
  1002. },
  1003. nextSongNotifications: {
  1004. name: 'Show a notification when a new song starts',
  1005. default: false
  1006. },
  1007. releaseReminder: {
  1008. name: 'Show new releases that I have saved',
  1009. default: true
  1010. },
  1011. keepLibrary: {
  1012. name: 'Store all visited or played albums',
  1013. default: true
  1014. },
  1015. darkMode: {
  1016. name: (CHROME ? '🅳🅐🆁🅺🅼🅞🅳🅴' : '🅳🅰🆁🅺🅼🅾🅳🅴') + ' - enable <a href="https://userstyles.org/styles/171538/bandcamp-in-dark">dark theme by Simonus</a>',
  1017. default: false
  1018. },
  1019. showAlbumID: {
  1020. name: 'Show album ID on album page',
  1021. default: false
  1022. },
  1023. feedShowOnlyNewReleases: {
  1024. name: 'Show only new releases in the feed',
  1025. default: false
  1026. },
  1027. feedShowAudioControls: {
  1028. name: 'Show play/pause/seek-bar in the feed',
  1029. default: true
  1030. },
  1031. customReleaseDateFormat: {
  1032. name: 'Format release date on album page',
  1033. default: false
  1034. }
  1035. };
  1036. const moreSettings = {
  1037. darkMode: {
  1038. true: async function populateDarkModeSettings(container) {
  1039. let darkModeValue = await GM.getValue('darkmode', '1');
  1040. const onChange = async function () {
  1041. const input = this;
  1042. window.setTimeout(() => parentQuery(input, 'fieldset').classList.add('breathe'), 0);
  1043. document.getElementById('bcsde_mode_auto_status').innerHTML = '';
  1044. document.getElementById('bcsde_mode_const_time_from').classList.remove('errorblink');
  1045. document.getElementById('bcsde_mode_const_time_to').classList.remove('errorblink');
  1046. if (document.getElementById('bcsde_mode_always').checked) {
  1047. darkModeValue = '1';
  1048. } else if (document.getElementById('bcsde_mode_const_time').checked) {
  1049. let from = document.getElementById('bcsde_mode_const_time_from').value;
  1050. let to = document.getElementById('bcsde_mode_const_time_to').value;
  1051. const mFrom = from.match(/([0-2]?\d:[0-5]\d)/);
  1052. const mTo = to.match(/([0-2]?\d:[0-5]\d)/);
  1053. if (mFrom && mTo) {
  1054. from = mFrom[1];
  1055. to = mTo[1];
  1056. document.getElementById('bcsde_mode_const_time_from').value = from;
  1057. document.getElementById('bcsde_mode_const_time_to').value = to;
  1058. darkModeValue = `2#${from}->${to}`;
  1059. } else {
  1060. if (!mFrom) {
  1061. document.getElementById('bcsde_mode_const_time_from').classList.add('errorblink');
  1062. }
  1063. if (!mTo) {
  1064. document.getElementById('bcsde_mode_const_time_to').classList.add('errorblink');
  1065. }
  1066. }
  1067. } else if (document.getElementById('bcsde_mode_auto').checked) {
  1068. let myPosition = null;
  1069. let sunData = null;
  1070. try {
  1071. myPosition = await getGPSLocation();
  1072. sunData = suntimes(new Date(), myPosition.latitude, myPosition.longitude);
  1073. } catch (e) {
  1074. document.getElementById('bcsde_mode_auto_status').innerHTML = 'Error:\n' + e;
  1075. }
  1076. if (myPosition && sunData) {
  1077. const data = Object.assign(myPosition, sunData);
  1078. darkModeValue = '3#' + JSON.stringify(data);
  1079. document.getElementById('bcsde_mode_auto_status').innerHTML = `Source: ${data.source}
  1080. Location: ${data.latitude}, ${data.longitude}
  1081. Sunrise: ${data.sunrise.toLocaleTimeString()}
  1082. Sunset: ${data.sunset.toLocaleTimeString()}`;
  1083. }
  1084. }
  1085. await GM.setValue('darkmode', darkModeValue);
  1086. window.setTimeout(() => parentQuery(input, 'fieldset').classList.remove('breathe'), 50);
  1087. };
  1088. const radioAlways = container.appendChild(document.createElement('input'));
  1089. radioAlways.setAttribute('type', 'radio');
  1090. radioAlways.setAttribute('name', 'mode');
  1091. radioAlways.setAttribute('value', 'always');
  1092. radioAlways.setAttribute('id', 'bcsde_mode_always');
  1093. radioAlways.checked = darkModeValue.startsWith('1');
  1094. radioAlways.addEventListener('change', onChange);
  1095. const labelAlways = container.appendChild(document.createElement('label'));
  1096. labelAlways.setAttribute('for', 'bcsde_mode_always');
  1097. labelAlways.appendChild(document.createTextNode('Always'));
  1098. container.appendChild(document.createElement('br'));
  1099. const radioConstTime = container.appendChild(document.createElement('input'));
  1100. radioConstTime.setAttribute('type', 'radio');
  1101. radioConstTime.setAttribute('name', 'mode');
  1102. radioConstTime.setAttribute('value', 'const_time');
  1103. radioConstTime.setAttribute('id', 'bcsde_mode_const_time');
  1104. radioConstTime.checked = darkModeValue.startsWith('2');
  1105. radioConstTime.addEventListener('change', onChange);
  1106. let [from, to] = ['22:00', '06:00'];
  1107. if (darkModeValue.startsWith('2')) {
  1108. [from, to] = darkModeValue.substring(2).split('->');
  1109. }
  1110. const labelConstTime = container.appendChild(document.createElement('label'));
  1111. labelConstTime.setAttribute('for', 'bcsde_mode_const_time');
  1112. labelConstTime.appendChild(document.createTextNode('Time'));
  1113. const labelConstTimeFrom = container.appendChild(document.createElement('label'));
  1114. labelConstTimeFrom.setAttribute('for', 'bcsde_mode_const_time_from');
  1115. labelConstTimeFrom.appendChild(document.createTextNode(' from '));
  1116. const inputConstTimeFrom = container.appendChild(document.createElement('input'));
  1117. inputConstTimeFrom.setAttribute('type', 'text');
  1118. inputConstTimeFrom.setAttribute('value', from);
  1119. inputConstTimeFrom.setAttribute('id', 'bcsde_mode_const_time_from');
  1120. inputConstTimeFrom.addEventListener('change', onChange);
  1121. const labelConstTimeTo = container.appendChild(document.createElement('label'));
  1122. labelConstTimeTo.setAttribute('for', 'bcsde_mode_const_time_to');
  1123. labelConstTimeTo.appendChild(document.createTextNode(' to '));
  1124. const inputConstTimeTo = container.appendChild(document.createElement('input'));
  1125. inputConstTimeTo.setAttribute('type', 'text');
  1126. inputConstTimeTo.setAttribute('value', to);
  1127. inputConstTimeTo.setAttribute('id', 'bcsde_mode_const_time_to');
  1128. inputConstTimeTo.addEventListener('change', onChange);
  1129. container.appendChild(document.createElement('br'));
  1130. const radioAuto = container.appendChild(document.createElement('input'));
  1131. radioAuto.setAttribute('type', 'radio');
  1132. radioAuto.setAttribute('name', 'mode');
  1133. radioAuto.setAttribute('value', 'auto');
  1134. radioAuto.setAttribute('id', 'bcsde_mode_auto');
  1135. radioAuto.checked = darkModeValue.startsWith('3');
  1136. radioAuto.addEventListener('change', onChange);
  1137. const labelAuto = container.appendChild(document.createElement('label'));
  1138. labelAuto.setAttribute('for', 'bcsde_mode_auto');
  1139. labelAuto.appendChild(document.createTextNode('Auto (sunset till sunrise)'));
  1140. const preAutoStatus = container.appendChild(document.createElement('pre'));
  1141. preAutoStatus.setAttribute('id', 'bcsde_mode_auto_status');
  1142. preAutoStatus.setAttribute('style', 'font-family:monospace');
  1143. return 'Dark theme details';
  1144. }
  1145. },
  1146. discographyplayerSidebar: {
  1147. true: function checkScreenSize(container) {
  1148. if (!window.matchMedia('(min-width: 1600px)').matches) {
  1149. const span = container.appendChild(document.createElement('span'));
  1150. span.appendChild(document.createTextNode('Your screen/browser window is not wide enough for this option. Width of at least 1600px required'));
  1151. container.style.opacity = 1;
  1152. } else {
  1153. container.style.opacity = 0;
  1154. }
  1155. return fullfill();
  1156. },
  1157. false: function removeContainerAboutScreenSize(container) {
  1158. container.style.opacity = 0;
  1159. return fullfill();
  1160. }
  1161. },
  1162. nextSongNotifications: {
  1163. true: async function populateNotificationSettings(container) {
  1164. const onChange = async function () {
  1165. const input = this;
  1166. document.getElementById('bcsde_notification_timeout').classList.remove('errorblink');
  1167. let seconds = -1;
  1168. try {
  1169. seconds = parseFloat(document.getElementById('bcsde_notification_timeout').value.trim());
  1170. } catch (e) {
  1171. seconds = -1;
  1172. }
  1173. if (seconds < 0) {
  1174. document.getElementById('bcsde_notification_timeout').classList.add('errorblink');
  1175. } else {
  1176. NOTIFICATION_TIMEOUT = parseInt(1000.0 * seconds);
  1177. await GM.setValue('notification_timeout', NOTIFICATION_TIMEOUT);
  1178. input.style.boxShadow = '2px 2px 5px #0a0f';
  1179. window.setTimeout(function resetBoxShadowTimeout() {
  1180. input.style.boxShadow = '';
  1181. }, 3000);
  1182. }
  1183. };
  1184. const labelTimeout = container.appendChild(document.createElement('label'));
  1185. labelTimeout.setAttribute('for', 'bcsde_notification_timeout');
  1186. labelTimeout.appendChild(document.createTextNode('Show for '));
  1187. const inputTimeout = container.appendChild(document.createElement('input'));
  1188. inputTimeout.setAttribute('type', 'text');
  1189. inputTimeout.setAttribute('size', '3');
  1190. inputTimeout.setAttribute('value', (await GM.getValue('notification_timeout', NOTIFICATION_TIMEOUT)) / 1000.0);
  1191. inputTimeout.setAttribute('id', 'bcsde_notification_timeout');
  1192. inputTimeout.addEventListener('change', onChange);
  1193. const labelPostTimeout = container.appendChild(document.createElement('label'));
  1194. labelPostTimeout.setAttribute('for', 'bcsde_notification_timeout');
  1195. labelPostTimeout.appendChild(document.createTextNode(' seconds (0 = show until manually closed or default value of browser)'));
  1196. }
  1197. },
  1198. customReleaseDateFormat: {
  1199. true: async function populateCustomReleaseDateFormatSettings(container) {
  1200. const defaultFormat = '%YYYY%.%MM%.%DD%';
  1201. const onChange = async function () {
  1202. const input = this;
  1203. document.getElementById('bcsde_custom_release_date_format_str').classList.remove('errorblink');
  1204. const customFormat = document.getElementById('bcsde_custom_release_date_format_str').value;
  1205. if (customFormat && customFormat.trim()) {
  1206. await GM.setValue('custom_release_date_format_str', customFormat.trim());
  1207. input.style.boxShadow = '2px 2px 5px #0a0f';
  1208. window.setTimeout(function resetBoxShadowTimeout() {
  1209. input.style.boxShadow = '';
  1210. }, 3000);
  1211. } else {
  1212. document.getElementById('bcsde_custom_release_date_format_str').classList.add('errorblink');
  1213. }
  1214. };
  1215. const onKeyUp = function () {
  1216. const customFormat = document.getElementById('bcsde_custom_release_date_format_str').value;
  1217. const preview = document.getElementById('bcsde_custom_release_date_preview');
  1218. if (customFormat && customFormat.trim()) {
  1219. preview.textContent = 'Preview: ' + customDateFormatter(customFormat.trim(), new Date(981154800000));
  1220. } else {
  1221. preview.textContent = 'Preview:';
  1222. }
  1223. };
  1224. const labelFormat = container.appendChild(document.createElement('label'));
  1225. labelFormat.setAttribute('for', 'bcsde_custom_release_date_format_str');
  1226. labelFormat.appendChild(document.createTextNode('Custom format: '));
  1227. const inputFormat = container.appendChild(document.createElement('input'));
  1228. inputFormat.setAttribute('type', 'text');
  1229. inputFormat.setAttribute('size', '40');
  1230. inputFormat.setAttribute('value', await GM.getValue('custom_release_date_format_str', defaultFormat));
  1231. inputFormat.setAttribute('id', 'bcsde_custom_release_date_format_str');
  1232. inputFormat.addEventListener('change', onChange);
  1233. inputFormat.addEventListener('change', onKeyUp);
  1234. inputFormat.addEventListener('keyup', onKeyUp);
  1235. container.appendChild(document.createElement('br'));
  1236. const preview = container.appendChild(document.createElement('span'));
  1237. preview.setAttribute('id', 'bcsde_custom_release_date_preview');
  1238. preview.readOnly = true;
  1239. container.appendChild(document.createElement('br'));
  1240. const link = container.appendChild(document.createElement('a'));
  1241. link.setAttribute('target', '_blank');
  1242. link.setAttribute('href', 'https://github.com/cvzi/Bandcamp-script-deluxe-edition/issues/284#issuecomment-1563394077');
  1243. link.appendChild(document.createTextNode('Format options: %DD%, %MM%, %YYYY%, ...'));
  1244. onKeyUp();
  1245. }
  1246. }
  1247. };
  1248. let player, audio, currentDuration, timeline, playhead, bufferbar;
  1249. let onPlayHead = false;
  1250. const spriteRepeatShuffle = "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACUAAABgCAMAAACt1UvuAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAA2UExURQAAAP////39/Tw8PP///////w4ODv////7+/v7+/k5OTktLS35+fiAgIJSUlAAAABAQECoqKpxAnVsAAAAPdFJOUwAxQ05UJGkKBRchgWiOOufd5UcAAAKrSURBVEjH7ZfrkqQgDIUbFLmphPd/2T2EgNqNzlTt7o+p3dR0d5V+JOGEYzkvZ63nsNY6517XCPIrjIDvXF7qL24ao5QynesIllDKE1MpJdom1UDBQIQlE+HmEipVIk+6cqVqQYivlq/loBJFDa6WnaitbbnMtFHnOF1niDJJX14pPa+cOm0l3Vohyuus8xpkj9ih1nPke6iaO6KV323XqwhRON4tQ3GedakNYYQqslaO+yv9xs64Lh2rX8sWeSISzVWTk8ROJmmU9MTl1PvEnHBmzXRSzvhhuqJAzjlJY9eJCVWljKwcESbL+fbTYK0NWx0IGodyvKCACqp6VqMNlguhktbxMqHdI5k7ps1SsiTxPO0YDgojkZPIysl+617cy8rUkIfPflMY4IaKLZfHhSoPn782iQJC5tIX2nfNQseGG4eoe3T1+kXh7j1j/H6W9TbC65ZxR2S0frKePUWYlhbY/hTkvL6aiKPApCRTeoxNTvUTI16r1DqPAqrGVR0UT/ojwGByJ6qO8S32HQ6wJ8r4TwFdyGnx7kzVM8l/nZpwRwkm1GAKC+5oKflMzY3aUm4rBpSsd17pVv2Bsn739ivqFWK2bhD2TE0wwTKM3Knu2puo1PJ8blqu7TEXVY1wgvGQwYN6HKJR0WGjYqxheN/lCpOzd/GlHX+gHyEe/SE/qpyV+sKPfqdEhzVv/OjwwC3zlefnnR+9YW+5Zz86fzjw3o+f1NCP9oMa+fGeOvnR2brH/378B/xI9A0/UjUjSfyOH2GzCDOuKavyUUM/eryMFjNOIMrHD/1o4di0GlCkp8IP/RjwglRSCKX9yI845VGXqwc18KOtWq3mSr35EQVnHbnzC3X144I3d7Wj6xuq+hH7gwz4PvY48GP9p8i2Vzus/dt+pB/nx18MUmsLM2EHrwAAAABJRU5ErkJggg==')";
  1251. function humanDuration(duration) {
  1252. let hours = parseInt(duration / 3600);
  1253. if (!hours) {
  1254. hours = '';
  1255. } else {
  1256. hours += ':';
  1257. }
  1258. duration %= 3600;
  1259. let minutes = parseInt(duration / 60);
  1260. minutes = (minutes < 10 ? '0' : '') + minutes;
  1261. duration %= 60;
  1262. let seconds = parseInt(duration);
  1263. if (duration - seconds >= 0.5) {
  1264. seconds++;
  1265. }
  1266. seconds = (seconds < 10 ? '0' : '') + seconds;
  1267. return `${hours}${minutes}:${seconds}`;
  1268. }
  1269. function humanBytes(bytes, precision) {
  1270. bytes = parseInt(bytes, 10);
  1271. if (bytes === 0) {
  1272. return '0 Byte';
  1273. }
  1274. const k = 1024;
  1275. const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  1276. const i = Math.floor(Math.log(bytes) / Math.log(k));
  1277. return parseFloat((bytes / Math.pow(k, i)).toPrecision(2)) + ' ' + sizes[i];
  1278. }
  1279. function addLogVolume(mediaElement) {
  1280. if (!Object.hasOwnProperty.call(mediaElement, 'logVolume')) {
  1281. Object.defineProperty(mediaElement, 'logVolume', {
  1282. get() {
  1283. return Math.log((Math.E - 1) * this.volume + 1);
  1284. },
  1285. set(percentage) {
  1286. this.volume = (Math.exp(percentage) - 1) / (Math.E - 1);
  1287. }
  1288. });
  1289. }
  1290. }
  1291. function sleep(t) {
  1292. return new Promise(resolve => setTimeout(resolve, t));
  1293. }
  1294. function randomIndex(max) {
  1295. // Random int from interval [0,max)
  1296. return Math.floor(Math.random() * Math.floor(max));
  1297. }
  1298. function padd(n, width, filler) {
  1299. let s;
  1300. for (s = n.toString(); s.length < width; s = filler + s);
  1301. return s;
  1302. }
  1303. function metricPrefix(n, decimals, k) {
  1304. // From http://stackoverflow.com/a/18650828
  1305. if (n <= 0) {
  1306. return String(n);
  1307. }
  1308. k = k || 1000;
  1309. const dm = decimals <= 0 ? 0 : decimals || 2;
  1310. const sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
  1311. const i = Math.floor(Math.log(n) / Math.log(k));
  1312. return parseFloat((n / Math.pow(k, i)).toFixed(dm)) + sizes[i];
  1313. }
  1314. function fixFilename(s) {
  1315. const forbidden = '*"/\\[]:|,<>?\n\t\0'.split('');
  1316. forbidden.forEach(function (char) {
  1317. s = s.replace(char, '');
  1318. });
  1319. return s;
  1320. }
  1321. function fullfill(x) {
  1322. return new Promise(resolve => resolve(x));
  1323. }
  1324. function customDateFormatter(format, date) {
  1325. const fields = {
  1326. '%isoDate%': () => date.toISOString(),
  1327. '%unix%': () => parseInt(date.getTime() / 1000),
  1328. '%YY%': () => date.getFullYear().toString().substring(2),
  1329. '%YYYY%': () => date.getFullYear(),
  1330. '%M%': () => date.getMonth() + 1,
  1331. '%MM%': () => padd(date.getMonth() + 1, 2, '0'),
  1332. '%Mon%': () => date.toLocaleString(undefined, {
  1333. month: 'short'
  1334. }),
  1335. '%Month%': () => date.toLocaleString(undefined, {
  1336. month: 'long'
  1337. }),
  1338. '%D%': () => date.getDate(),
  1339. '%DD%': () => padd(date.getDate(), 2, '0'),
  1340. '%Da%': () => date.toLocaleString(undefined, {
  1341. weekday: 'short'
  1342. }),
  1343. '%Day%': () => date.toLocaleString(undefined, {
  1344. weekday: 'long'
  1345. }),
  1346. '%Dord%': () => date.getDate() + (date.getDate() % 10 === 1 && date.getDate() !== 11 ? 'st' : date.getDate() % 10 === 2 && date.getDate() !== 12 ? 'nd' : date.getDate() % 10 === 3 && date.getDate() !== 13 ? 'rd' : 'th'),
  1347. '%json%': () => date.toJSON()
  1348. };
  1349. for (const field in fields) {
  1350. if (format.includes(field)) {
  1351. try {
  1352. format = format.replace(field, fields[field]());
  1353. } catch (e) {
  1354. console.error('customDateFormatter: Could not format replace "' + field + '": ' + e);
  1355. }
  1356. }
  1357. }
  1358. return format;
  1359. }
  1360. const stylesToInsert = [];
  1361. function addStyle(css) {
  1362. if (GM_addStyle && css) {
  1363. return GM_addStyle(css);
  1364. } else {
  1365. if (css) {
  1366. stylesToInsert.push(css);
  1367. }
  1368. const head = document.head ? document.head : document.documentElement;
  1369. if (head) {
  1370. let style = document.createElement('style');
  1371. if (style) {
  1372. while (stylesToInsert.length) {
  1373. head.append(style);
  1374. style.type = 'text/css';
  1375. style.appendChild(document.createTextNode(stylesToInsert.shift()));
  1376. style = document.createElement('style');
  1377. }
  1378. return fullfill(style);
  1379. }
  1380. }
  1381. // document was not ready, wait
  1382. return new Promise(resolve => window.setTimeout(() => addStyle(false).then(resolve), 100));
  1383. }
  1384. }
  1385. function css2rgb(colorStr) {
  1386. const div = document.body.appendChild(document.createElement('div'));
  1387. div.style.color = colorStr;
  1388. const m = window.getComputedStyle(div).color.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i);
  1389. div.remove();
  1390. if (m) {
  1391. m.shift();
  1392. return m;
  1393. }
  1394. return null;
  1395. }
  1396. function base64encode(s) {
  1397. // from https://gist.github.com/stubbetje/229984
  1398. const base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split('');
  1399. const l = s.length;
  1400. let o = '';
  1401. for (let i = 0; i < l; i++) {
  1402. const byte0 = s.charCodeAt(i++) & 0xff;
  1403. const byte1 = s.charCodeAt(i++) & 0xff;
  1404. const byte2 = s.charCodeAt(i) & 0xff;
  1405. o += base64[byte0 >> 2];
  1406. o += base64[(byte0 & 0x3) << 4 | byte1 >> 4];
  1407. const t = i - l;
  1408. if (t >= 0) {
  1409. if (t === 0) {
  1410. o += base64[(byte1 & 0x0f) << 2 | byte2 >> 6];
  1411. o += base64[64];
  1412. } else {
  1413. o += base64[64];
  1414. o += base64[64];
  1415. }
  1416. } else {
  1417. o += base64[(byte1 & 0x0f) << 2 | byte2 >> 6];
  1418. o += base64[byte2 & 0x3f];
  1419. }
  1420. }
  1421. return o;
  1422. }
  1423. function decodeHTMLentities(input) {
  1424. return new window.DOMParser().parseFromString(input, 'text/html').documentElement.textContent;
  1425. }
  1426. function timeSince(date) {
  1427. // https://stackoverflow.com/a/72973090/
  1428. const MINUTE = 60;
  1429. const HOUR = MINUTE * 60;
  1430. const DAY = HOUR * 24;
  1431. const WEEK = DAY * 7;
  1432. const MONTH = DAY * 30;
  1433. const YEAR = DAY * 365;
  1434. const secondsAgo = Math.round((Date.now() - Number(date)) / 1000);
  1435. if (secondsAgo < MINUTE) {
  1436. return secondsAgo + ` second${secondsAgo !== 1 ? 's' : ''} ago`;
  1437. }
  1438. let divisor;
  1439. let unit = '';
  1440. if (secondsAgo < HOUR) {
  1441. [divisor, unit] = [MINUTE, 'minute'];
  1442. } else if (secondsAgo < DAY) {
  1443. [divisor, unit] = [HOUR, 'hour'];
  1444. } else if (secondsAgo < WEEK) {
  1445. [divisor, unit] = [DAY, 'day'];
  1446. } else if (secondsAgo < MONTH) {
  1447. [divisor, unit] = [WEEK, 'week'];
  1448. } else if (secondsAgo < YEAR) {
  1449. [divisor, unit] = [MONTH, 'month'];
  1450. } else {
  1451. [divisor, unit] = [YEAR, 'year'];
  1452. }
  1453. const count = Math.floor(secondsAgo / divisor);
  1454. return `${count} ${unit}${count > 1 ? 's' : ''} ago`;
  1455. }
  1456. function nowInTimeRange(range) {
  1457. // Format: range = 'hh:mm->hh:mm'
  1458. const m = range.match(/(\d{1,2}):(\d{1,2})->(\d{1,2}):(\d{1,2})/);
  1459. const [fromHours, fromMinutes, toHours, toMinutes] = [parseInt(m[1]), parseInt(m[2]), parseInt(m[3]), parseInt(m[4])];
  1460. const now = new Date();
  1461. const from = new Date();
  1462. from.setHours(fromHours);
  1463. from.setMinutes(fromMinutes);
  1464. const to = new Date();
  1465. to.setHours(toHours);
  1466. to.setMinutes(toMinutes);
  1467. if (to - from < 0) {
  1468. to.setDate(to.getDate() + 1);
  1469. }
  1470. return now > from && now < to;
  1471. }
  1472. function nowInBetween(from, to) {
  1473. const time = new Date();
  1474. const start = from.getHours() * 60 + from.getMinutes();
  1475. const end = to.getHours() * 60 + to.getMinutes();
  1476. const now = time.getHours() * 60 + time.getMinutes();
  1477. if (start >= end) {
  1478. return start <= now && now >= end || start >= now && now <= end;
  1479. } else {
  1480. return start <= now && now <= end;
  1481. }
  1482. }
  1483. function loadCrossSiteImage(url) {
  1484. return new Promise(function downloadCrossSiteImage(resolve, reject) {
  1485. const canvas = document.createElement('canvas');
  1486. const ctx = canvas.getContext('2d');
  1487. const img0 = document.createElement('img'); // Load the image in a <img> to get the dimensions
  1488. img0.addEventListener('load', function onImgLoad() {
  1489. if (img0.height === 0 || img0.width === 0) {
  1490. reject(new Error('loadCrossSiteImage("$url") Error: Could not load image in <img>'));
  1491. return;
  1492. }
  1493. canvas.height = img0.height;
  1494. canvas.width = img0.width;
  1495. // Download image data
  1496. GM.xmlHttpRequest({
  1497. method: 'GET',
  1498. overrideMimeType: 'text/plain; charset=x-user-defined',
  1499. url,
  1500. onload: function (resp) {
  1501. // Create a data url image
  1502. const dataurl = 'data:image/jpeg;base64,' + base64encode(resp.responseText);
  1503. const img1 = document.createElement('img');
  1504. img1.addEventListener('load', function () {
  1505. // Load data url image into canvas
  1506. ctx.drawImage(img1, 0, 0);
  1507. resolve(canvas);
  1508. });
  1509. img1.src = dataurl;
  1510. },
  1511. onerror: function (response) {
  1512. console.error('loadCrossSiteImage("' + url + '") Error: ' + response.status + '\n' + ('error' in response ? response.error : ''));
  1513. reject(new Error('error' in response ? response.error : 'loadCrossSiteImage failed'));
  1514. }
  1515. });
  1516. });
  1517. img0.src = url;
  1518. });
  1519. }
  1520. function removeViaQuerySelector(parent, selector) {
  1521. if (typeof selector === 'undefined') {
  1522. selector = parent;
  1523. parent = document;
  1524. }
  1525. for (let el = parent.querySelector(selector); el; el = parent.querySelector(selector)) {
  1526. el.remove();
  1527. }
  1528. }
  1529. function firstChildWithText(parent) {
  1530. for (let i = 0; i < parent.childNodes.length; i++) {
  1531. const node = parent.childNodes[i];
  1532. if (node.nodeType === window.Node.TEXT_NODE && node.nodeValue.trim()) {
  1533. return node;
  1534. } else if (node.childNodes.length) {
  1535. const r = firstChildWithText(node);
  1536. if (r) {
  1537. return r;
  1538. }
  1539. }
  1540. }
  1541. return false;
  1542. }
  1543. function parentQuery(node, q) {
  1544. const parents = [node.parentElement];
  1545. node = node.parentElement.parentElement;
  1546. while (node) {
  1547. const lst = node.querySelectorAll(q);
  1548. for (let i = 0; i < lst.length; i++) {
  1549. if (parents.indexOf(lst[i]) !== -1) {
  1550. return lst[i];
  1551. }
  1552. }
  1553. parents.push(node);
  1554. node = node.parentElement;
  1555. }
  1556. return null;
  1557. }
  1558. function suntimes(date, lat, lng) {
  1559. // According to "Predicting Sunrise and Sunset Times" by Donald A. Teets:
  1560. // https://www.maa.org/sites/default/files/teets09010341463.pdf
  1561. lat = lat * Math.PI / 180.0;
  1562. const dayOfYear = Math.round((date - new Date(date).setMonth(0, 0)) / 86400000);
  1563. const sunDist = 149598000.0;
  1564. const radius = 6378.0;
  1565. const epsilon = 0.409;
  1566. const thetha = 2 * Math.PI / 365.25 * (dayOfYear - 80);
  1567. const n = 720 - 10 * Math.sin(2 * thetha) + 8 * Math.sin(2 * Math.PI / 365.25 * dayOfYear);
  1568. const z = sunDist * Math.sin(thetha) * Math.sin(epsilon);
  1569. const rp = Math.sqrt(sunDist * sunDist - z * z);
  1570. const t0 = 1440 / (2 * Math.PI) * Math.acos((radius - z * Math.sin(lat)) / (rp * Math.cos(lat)));
  1571. const sunriseMin = n - t0 - 5 - 4.0 * lng % 15.0 - date.getTimezoneOffset();
  1572. const sunsetMin = sunriseMin + 2 * t0;
  1573. const sunrise = new Date(date);
  1574. sunrise.setHours(sunriseMin / 60, Math.round(sunriseMin % 60));
  1575. const sunset = new Date(date);
  1576. sunset.setHours(sunsetMin / 60, Math.round(sunsetMin % 60));
  1577. return {
  1578. sunrise,
  1579. sunset
  1580. };
  1581. }
  1582. function fromISO6709(s) {
  1583. // Format: s = '+-DDMM+-DDDMM'
  1584. // Format: s = '+-DDMMSS+-DDDMMSS'
  1585. function convert(iso, negative) {
  1586. const mm = iso % 100;
  1587. const dd = iso / 100;
  1588. return (dd + mm / 60) * (negative ? -1 : 1);
  1589. }
  1590. const m = s.match(/([+-])(\d+)([+-])(\d+)/);
  1591. const lat = convert(parseInt(m[2]), m[1] === '-');
  1592. const lng = convert(parseInt(m[4]), m[3] === '-');
  1593. return {
  1594. latitude: lat,
  1595. longitude: lng
  1596. };
  1597. }
  1598. function getGPSLocation() {
  1599. return new Promise(function downloadCrossSiteImage(resolve, reject) {
  1600. navigator.geolocation.getCurrentPosition(function onSuccess(position) {
  1601. resolve({
  1602. source: `navigator.geolocation@${new Date(position.timestamp).toLocaleString()}`,
  1603. latitude: position.coords.latitude,
  1604. longitude: position.coords.longitude
  1605. });
  1606. }, function onError(err) {
  1607. console.error('getGPSLocation Error:', err);
  1608. const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
  1609. console.debug('getGPSLocation: Timezone: ' + tz);
  1610. GM.xmlHttpRequest({
  1611. method: 'GET',
  1612. url: 'https://raw.githubusercontent.com/iospirit/NSTimeZone-ISCLLocation/master/zone.tab',
  1613. onload: function (response) {
  1614. if (response.responseText.indexOf(tz) !== -1) {
  1615. const line = response.responseText.split(tz)[0].split('\n').pop();
  1616. const myPosition = fromISO6709(line);
  1617. myPosition.source = 'Browser timezone ' + tz;
  1618. resolve(myPosition);
  1619. } else if (response.status !== 200) {
  1620. reject(new Error('Could not download time zone locations: http status=' + response.status));
  1621. } else {
  1622. reject(new Error('Unkown time zone location: ' + tz));
  1623. }
  1624. },
  1625. onerror: function (response) {
  1626. reject(new Error('Could not download time zone locations: ' + response.error));
  1627. }
  1628. });
  1629. });
  1630. });
  1631. }
  1632. const _dateOptions = {
  1633. year: 'numeric',
  1634. month: 'short',
  1635. day: 'numeric'
  1636. };
  1637. const _dateOptionsWithoutYear = {
  1638. month: 'short',
  1639. day: 'numeric'
  1640. };
  1641. const _dateOptionsNumericWithoutYear = {
  1642. year: '2-digit',
  1643. month: '2-digit',
  1644. day: '2-digit'
  1645. };
  1646. function dateFormater(date) {
  1647. if (date.getFullYear() === new Date().getFullYear()) {
  1648. return date.toLocaleDateString(undefined, _dateOptionsWithoutYear);
  1649. } else {
  1650. return date.toLocaleDateString(undefined, _dateOptions);
  1651. }
  1652. }
  1653. function dateFormaterRelease(date) {
  1654. return date.toLocaleDateString(undefined, _dateOptionsWithoutYear) + ', ' + date.getFullYear();
  1655. }
  1656. function dateFormaterNumeric(date) {
  1657. return date.toLocaleDateString(undefined, _dateOptionsNumericWithoutYear);
  1658. }
  1659. let enabledFeaturesLoaded = false;
  1660. function getEnabledFeatures(enabledFeaturesValue) {
  1661. for (const feature in allFeatures) {
  1662. allFeatures[feature].enabled = allFeatures[feature].default;
  1663. }
  1664. if (enabledFeaturesValue !== false) {
  1665. const enabledFeatures = JSON.parse(enabledFeaturesValue);
  1666. if (enabledFeatures.constructor === Object) {
  1667. for (const feature in enabledFeatures) {
  1668. if (feature in allFeatures) {
  1669. allFeatures[feature].enabled = enabledFeatures[feature].enabled;
  1670. }
  1671. }
  1672. }
  1673. }
  1674. enabledFeaturesLoaded = true;
  1675. return allFeatures;
  1676. }
  1677. function findUserProfileUrl() {
  1678. if (document.querySelector('#collection-main a')) {
  1679. return document.querySelector('#collection-main a').href;
  1680. }
  1681. return 'https://bandcamp.com/login';
  1682. }
  1683. let ivRestoreVolume;
  1684. function getStoredVolume(callbackIfVolumeExists) {
  1685. GM.getValue('volume', '0.7').then(str => {
  1686. return parseFloat(str);
  1687. }).then(function storedVolumeLoaded(volume) {
  1688. if (!Number.isNaN(volume) && volume > 0.0) {
  1689. callbackIfVolumeExists(volume);
  1690. }
  1691. });
  1692. }
  1693. function restoreVolume() {
  1694. getStoredVolume(function getStoredVolumeCallback(volume) {
  1695. const restoreVolumeInterval = function restoreInterval() {
  1696. const audios = document.querySelectorAll('audio,video');
  1697. if (audios.length > 0) {
  1698. let paused = true;
  1699. audios.forEach(function (media) {
  1700. addLogVolume(media);
  1701. paused = paused && media.paused;
  1702. media.logVolume = volume;
  1703. });
  1704. if (!paused) {
  1705. // Clear interval once audio is actually playing
  1706. window.clearInterval(ivRestoreVolume);
  1707. }
  1708. // Update volume bar on tag player (by double clicking mute button)
  1709. const muteWrapper = document.querySelector('.vol-icon-wrapper');
  1710. if (muteWrapper) {
  1711. const mouseDownEvent = new MouseEvent('mousedown', {
  1712. view: unsafeWindow,
  1713. bubbles: true,
  1714. cancelable: true
  1715. });
  1716. muteWrapper.dispatchEvent(mouseDownEvent);
  1717. muteWrapper.dispatchEvent(mouseDownEvent);
  1718. }
  1719. }
  1720. };
  1721. restoreVolumeInterval();
  1722. ivRestoreVolume = window.setInterval(restoreVolumeInterval, 500);
  1723. });
  1724. window.setTimeout(function clearRestoreInterval() {
  1725. window.clearInterval(ivRestoreVolume);
  1726. }, 7000);
  1727. }
  1728. function findPreviousAlbumCover(currentUrl) {
  1729. const currentKey = albumKey(currentUrl);
  1730. const as = document.querySelectorAll('.music-grid .music-grid-item a[href*="/album/"],.music-grid .music-grid-item a[href*="/track/"]');
  1731. let last = false;
  1732. let found = false;
  1733. for (let i = 0; i < as.length; i++) {
  1734. if (last && albumKey(as[i].href) === currentKey) {
  1735. found = last;
  1736. break;
  1737. }
  1738. last = as[i];
  1739. }
  1740. if (found) {
  1741. return playAlbumFromCover.apply(found, null);
  1742. }
  1743. return false;
  1744. }
  1745. function findNextAlbumCover(currentUrl) {
  1746. const currentKey = albumKey(currentUrl);
  1747. const as = document.querySelectorAll('.music-grid .music-grid-item a[href*="/album/"],.music-grid .music-grid-item a[href*="/track/"]');
  1748. let isNext = false;
  1749. for (let i = 0; i < as.length; i++) {
  1750. if (isNext) {
  1751. playAlbumFromCover.apply(as[i], null);
  1752. return true;
  1753. }
  1754. if (albumKey(as[i].href) === currentKey) {
  1755. isNext = true;
  1756. }
  1757. }
  1758. return false;
  1759. }
  1760. const shufflePlayed = [];
  1761. function musicPlayerNextSong(next) {
  1762. const current = player.querySelector('.playlist .playing');
  1763. if (!next) {
  1764. if (player.querySelector('.shufflebutton').classList.contains('active')) {
  1765. // Shuffle mode
  1766. const allLoadedSongs = document.querySelectorAll('.playlist .playlistentry');
  1767. // Set a random song (that is not the current song and not in shufflePlayed)
  1768. let index = null;
  1769. for (let i = 0; i < 10; i++) {
  1770. index = randomIndex(allLoadedSongs.length);
  1771. const file = allLoadedSongs[index].dataset.file;
  1772. if (file !== current.dataset.file && shufflePlayed.indexOf(file) !== -1) {
  1773. break;
  1774. }
  1775. }
  1776. next = allLoadedSongs[index];
  1777. shufflePlayed.push(next.dataset.file);
  1778. } else {
  1779. // Normal mode
  1780. next = current.nextElementSibling;
  1781. while (next) {
  1782. if ('file' in next.dataset) {
  1783. break;
  1784. }
  1785. next = next.nextElementSibling;
  1786. }
  1787. }
  1788. }
  1789. if (next) {
  1790. current.classList.remove('playing');
  1791. next.classList.add('playing');
  1792. musicPlayerPlaySong(next);
  1793. } else {
  1794. // End of playlist reached
  1795. if (findNextAlbumCover(current.dataset.albumUrl) === false) {
  1796. const notloaded = player.querySelector('.playlist .playlistheading a.notloaded');
  1797. if (notloaded) {
  1798. // Unloaded albums in playlist
  1799. const url = notloaded.href;
  1800. notloaded.remove();
  1801. cachedTralbumData(url).then(function onCachedTralbumDataLoaded(TralbumData) {
  1802. if (TralbumData) {
  1803. addAlbumToPlaylist(TralbumData);
  1804. } else {
  1805. playAlbumFromUrl(url);
  1806. }
  1807. });
  1808. } else {
  1809. audio.pause();
  1810. audio.currentTime -= 1;
  1811. musicPlayerOnTimeUpdate();
  1812. window.alert('End of playlist reached');
  1813. }
  1814. }
  1815. }
  1816. }
  1817. let ivSlideInNextSong;
  1818. function musicPlayerPlaySong(next, startTime) {
  1819. currentDuration = next.dataset.duration;
  1820. player.querySelector('.durationDisplay .current').innerHTML = '-';
  1821. player.querySelector('.durationDisplay .total').innerHTML = humanDuration(currentDuration);
  1822. audio.src = next.dataset.file;
  1823. if (typeof startTime !== 'undefined' && startTime !== false) {
  1824. audio.currentTime = startTime;
  1825. }
  1826. bufferbar.classList.remove('bufferbaranimation');
  1827. window.setTimeout(function bufferbaranimationWidth() {
  1828. bufferbar.style.width = '0px';
  1829. window.setTimeout(function bufferbaranimationClass() {
  1830. bufferbar.classList.add('bufferbaranimation');
  1831. }, 10);
  1832. }, 0);
  1833. const key = albumKey(next.dataset.albumUrl);
  1834.  
  1835. // Meta
  1836. const currentlyPlaying = document.querySelector('.currentlyPlaying');
  1837. const nextInRow = player.querySelector('.nextInRow');
  1838. nextInRow.querySelector('.cover').href = next.dataset.albumUrl;
  1839. nextInRow.querySelector('.cover img').src = next.dataset.albumCover;
  1840. nextInRow.querySelector('.info .link').href = next.dataset.albumUrl;
  1841. nextInRow.querySelector('.info .title').innerHTML = next.dataset.title;
  1842. nextInRow.querySelector('.info .artist').innerHTML = next.dataset.artist;
  1843. nextInRow.querySelector('.info .album').innerHTML = next.dataset.album;
  1844.  
  1845. // Favicon
  1846. musicPlayerFavicon(next.dataset.albumCover.replace(/_\d.jpg$/, '_3.jpg'));
  1847.  
  1848. // Wishlist
  1849. const collectWishlist = player.querySelector('.collect-wishlist');
  1850. collectWishlist.dataset.albumUrl = next.dataset.albumUrl;
  1851. player.querySelectorAll('.collect-wishlist>*').forEach(function (e) {
  1852. e.style.display = 'none';
  1853. });
  1854. if (next.dataset.isPurchased === 'true') {
  1855. player.querySelector('.collect-wishlist .wishlist-own').style.display = 'inline-block';
  1856. collectWishlist.dataset.wishlist = 'own';
  1857. } else if (next.dataset.inWishlist === 'true') {
  1858. player.querySelector('.collect-wishlist .wishlist-collected').style.display = 'inline-block';
  1859. collectWishlist.dataset.wishlist = 'collected';
  1860. } else {
  1861. // Always show whishlist button for whole album
  1862. player.querySelector('.collect-wishlist .wishlist-add').style.display = 'inline-block';
  1863. player.querySelector('.collect-wishlist .wishlist-add .album').style.display = 'inline';
  1864. collectWishlist.dataset.wishlist = 'add';
  1865. if (next.dataset.isDownloadable === 'true' && next.dataset.trackUrl) {
  1866. // Only show wishlist button for single track if the track is downloadable and there is a track url
  1867. collectWishlist.dataset.trackUrl = next.dataset.trackUrl;
  1868. player.querySelector('.collect-wishlist .wishlist-add .track').style.display = 'inline';
  1869. player.querySelector('.collect-wishlist .wishlist-add .slash').style.display = 'inline';
  1870. } else {
  1871. player.querySelector('.collect-wishlist .wishlist-add .track').style.display = 'none';
  1872. player.querySelector('.collect-wishlist .wishlist-add .slash').style.display = 'none';
  1873. }
  1874. }
  1875.  
  1876. // Played/Listened
  1877. const collectListened = player.querySelector('.collect-listened');
  1878. if (allFeatures.markasplayed.enabled && collectListened) {
  1879. collectListened.dataset.albumUrl = next.dataset.albumUrl;
  1880. player.querySelectorAll('.collect-listened>*').forEach(function (e) {
  1881. e.style.display = 'none';
  1882. });
  1883. GM.getValue('myalbums', '{}').then(function myalbumsLoaded(str) {
  1884. const myalbums = JSON.parse(str);
  1885. if (key in myalbums && 'listened' in myalbums[key] && myalbums[key].listened) {
  1886. player.querySelector('.collect-listened .listened').style.display = 'inline-block';
  1887. const date = new Date(myalbums[key].listened);
  1888. const since = timeSince(date);
  1889. player.querySelector('.collect-listened .listened').title = since + ' ago\nClick to mark as NOT played';
  1890. collectListened.dataset.listened = myalbums[key].listened;
  1891. } else {
  1892. player.querySelector('.collect-listened .mark-listened').style.display = 'inline-block';
  1893. collectListened.dataset.listened = false;
  1894. }
  1895. });
  1896. } else if (collectListened) {
  1897. collectListened.remove();
  1898. }
  1899.  
  1900. // Notification
  1901. if (allFeatures.nextSongNotifications.enabled && 'notification' in GM) {
  1902. GM.notification({
  1903. title: document.location.host,
  1904. text: next.dataset.title + '\nby ' + next.dataset.artist + '\nfrom ' + next.dataset.album,
  1905. image: next.dataset.albumCover,
  1906. highlight: false,
  1907. silent: true,
  1908. timeout: NOTIFICATION_TIMEOUT,
  1909. onclick: musicPlayerNext
  1910. });
  1911. }
  1912.  
  1913. // Media hub
  1914. if ('mediaSession' in navigator) {
  1915. navigator.mediaSession.metadata = new MediaMetadata({
  1916. title: next.dataset.title,
  1917. artist: next.dataset.artist,
  1918. album: next.dataset.album,
  1919. artwork: [{
  1920. src: next.dataset.albumCover,
  1921. sizes: '350x350',
  1922. type: 'image/jpeg'
  1923. }]
  1924. });
  1925. navigator.mediaSession.setActionHandler('previoustrack', musicPlayerPrev);
  1926. navigator.mediaSession.setActionHandler('nexttrack', musicPlayerNext);
  1927. navigator.mediaSession.setActionHandler('play', _ => audio.play());
  1928. navigator.mediaSession.setActionHandler('pause', _ => audio.pause());
  1929. navigator.mediaSession.setActionHandler('seekbackward', function (event) {
  1930. const skipTime = event.seekOffset || DEFAULTSKIPTIME;
  1931. audio.currentTime = Math.max(audio.currentTime - skipTime, 0);
  1932. musicPlayerUpdatePositionState();
  1933. });
  1934. navigator.mediaSession.setActionHandler('seekforward', function (event) {
  1935. const skipTime = event.seekOffset || DEFAULTSKIPTIME;
  1936. audio.currentTime = Math.min(audio.currentTime + skipTime, audio.duration || currentDuration);
  1937. musicPlayerUpdatePositionState();
  1938. });
  1939. try {
  1940. navigator.mediaSession.setActionHandler('stop', _ => musicPlayerClose());
  1941. } catch (error) {
  1942. console.warn('Warning! The "stop" media session action is not supported.');
  1943. }
  1944. try {
  1945. navigator.mediaSession.setActionHandler('seekto', function (event) {
  1946. if (event.fastSeek && 'fastSeek' in audio) {
  1947. audio.fastSeek(event.seekTime);
  1948. return;
  1949. }
  1950. audio.currentTime = event.seekTime;
  1951. musicPlayerUpdatePositionState();
  1952. });
  1953. } catch (error) {
  1954. console.warn('Warning! The "seekto" media session action is not supported.');
  1955. }
  1956. }
  1957.  
  1958. // Download link
  1959. const downloadLink = player.querySelector('.downloadlink');
  1960. if (allFeatures.discographyplayerDownloadLink.enabled) {
  1961. downloadLink.href = next.dataset.file;
  1962. downloadLink.download = (next.dataset.trackNumber > 9 ? '' : '0') + next.dataset.trackNumber + '. ' + fixFilename(next.dataset.artist + ' - ' + next.dataset.title) + '.mp3';
  1963. downloadLink.style.display = 'block';
  1964. } else {
  1965. downloadLink.style.display = 'none';
  1966. }
  1967.  
  1968. // Show "playing" indication on album covers
  1969. let coverLinkPattern = albumPath(next.dataset.albumUrl);
  1970. if (document.location.href.split('.')[0] !== next.dataset.albumUrl.split('.')[0]) {
  1971. /*
  1972. Subdomain is different from album subdomain -> multiple artists on this page, use full url to detect albums.
  1973. Otherwise albums with the same name but a different artist name will be highlighted.
  1974. This would happen quite often on search results.
  1975. */
  1976. coverLinkPattern = albumKey(next.dataset.albumUrl);
  1977. }
  1978. document.querySelectorAll('img.albumIsCurrentlyPlaying').forEach(img => img.classList.remove('albumIsCurrentlyPlaying'));
  1979. document.querySelectorAll('.albumIsCurrentlyPlayingIndicator').forEach(div => div.remove());
  1980. document.querySelectorAll('a[href*="' + coverLinkPattern + '"] img,.info>a[href*="' + coverLinkPattern + '"]').forEach(function (img) {
  1981. let node = img;
  1982. while (node) {
  1983. if (node.id === 'discographyplayer') {
  1984. return;
  1985. }
  1986. if (node === document.body) {
  1987. break;
  1988. }
  1989. node = node.parentNode;
  1990. }
  1991. if (img.tagName === 'A') {
  1992. img = img.parentNode.parentNode.querySelector('.art img');
  1993. }
  1994. img.classList.add('albumIsCurrentlyPlaying');
  1995. if (!img.parentNode.querySelector('.albumIsCurrentlyPlayingIndicator')) {
  1996. const indicator = img.parentNode.appendChild(document.createElement('div'));
  1997. indicator.classList.add('albumIsCurrentlyPlayingIndicator');
  1998. indicator.addEventListener('click', function (ev) {
  1999. ev.preventDefault();
  2000. ev.stopPropagation();
  2001. if (!musicPlayerPlay()) {
  2002. // Album is now paused -> Remove indicators
  2003. document.querySelectorAll('img.albumIsCurrentlyPlaying').forEach(img => img.classList.remove('albumIsCurrentlyPlaying'));
  2004. document.querySelectorAll('.albumIsCurrentlyPlayingIndicator').forEach(div => div.remove());
  2005. }
  2006. });
  2007. indicator.appendChild(document.createElement('div')).classList.add('currentlyPlayingBg');
  2008. indicator.appendChild(document.createElement('div')).classList.add('currentlyPlayingIcon');
  2009. }
  2010. });
  2011.  
  2012. // Animate
  2013. if (allFeatures.discographyplayerSidebar.enabled && window.matchMedia('(min-width: 1600px)').matches) {
  2014. // Slide up
  2015. currentlyPlaying.style.marginTop = -parseInt(currentlyPlaying.clientHeight + 1) + 'px';
  2016. nextInRow.style.height = '99%';
  2017. nextInRow.style.width = '99%';
  2018. window.clearTimeout(ivSlideInNextSong);
  2019. ivSlideInNextSong = window.setTimeout(function slideInSongInterval() {
  2020. currentlyPlaying.remove();
  2021. const clone = nextInRow.cloneNode(true);
  2022. clone.style.height = '0%';
  2023. clone.className = 'nextInRow';
  2024. nextInRow.className = 'currentlyPlaying';
  2025. nextInRow.parentNode.appendChild(clone);
  2026. }, 600);
  2027. } else {
  2028. // Slide to the left
  2029. currentlyPlaying.style.marginLeft = -parseInt(currentlyPlaying.clientWidth + 1) + 'px';
  2030. nextInRow.style.height = '99%';
  2031. nextInRow.style.width = '99%';
  2032. window.clearTimeout(ivSlideInNextSong);
  2033. ivSlideInNextSong = window.setTimeout(function slideInSongInterval() {
  2034. currentlyPlaying.remove();
  2035. const clone = nextInRow.cloneNode(true);
  2036. clone.style.width = '0%';
  2037. clone.className = 'nextInRow';
  2038. nextInRow.className = 'currentlyPlaying';
  2039. nextInRow.parentNode.appendChild(clone);
  2040. }, 7 * 1000);
  2041. }
  2042. window.setTimeout(() => player.querySelector('.playlist .playing').scrollIntoView({
  2043. block: 'nearest'
  2044. }), 200);
  2045. }
  2046. function musicPlayerPlay() {
  2047. if (audio.paused) {
  2048. audio.play().then(_ => musicPlayerUpdatePositionState());
  2049. musicPlayerCookieChannelSendStop();
  2050. return true;
  2051. } else {
  2052. audio.pause();
  2053. document.querySelectorAll('img.albumIsCurrentlyPlaying').forEach(img => img.classList.remove('albumIsCurrentlyPlaying'));
  2054. document.querySelectorAll('.albumIsCurrentlyPlayingIndicator').forEach(div => div.remove());
  2055. return false;
  2056. }
  2057. }
  2058. function musicPlayerStop() {
  2059. if (!audio.paused) {
  2060. audio.pause();
  2061. }
  2062. }
  2063. function musicPlayerPrev() {
  2064. musicPlayerShowBusy();
  2065. const current = player.querySelector('.playlist .playing');
  2066. let prev = current.previousElementSibling;
  2067. while (prev) {
  2068. if ('file' in prev.dataset) {
  2069. break;
  2070. }
  2071. prev = prev.previousElementSibling;
  2072. }
  2073. if (prev) {
  2074. musicPlayerNextSong(prev);
  2075. }
  2076. }
  2077. function musicPlayerNext() {
  2078. musicPlayerShowBusy();
  2079. musicPlayerNextSong();
  2080. }
  2081. function musicPlayerPrevAlbum() {
  2082. audio.pause();
  2083. window.setTimeout(function musicPlayerPrevAlbumTimeout() {
  2084. musicPlayerShowBusy();
  2085. const url = player.querySelector('.playlist .playing').dataset.albumUrl;
  2086. if (!findPreviousAlbumCover(url)) {
  2087. // Find previous album in playlist
  2088. let prev = false;
  2089. const as = player.querySelectorAll('.playlist .playlistheading a');
  2090. for (let i = 0; i < as.length; i++) {
  2091. if (albumKey(as[i].href) === albumKey(url)) {
  2092. if (i > 0) {
  2093. prev = as[i - 1];
  2094. }
  2095. break;
  2096. }
  2097. }
  2098. if (prev) {
  2099. prev.parentNode.click();
  2100. } else {
  2101. // Just play first song in playlist
  2102. player.querySelector('.playlist .playlistentry').click();
  2103. }
  2104. }
  2105. }, 10);
  2106. }
  2107. function musicPlayerNextAlbum() {
  2108. audio.pause();
  2109. window.setTimeout(function musicPlayerNextAlbumTimeout() {
  2110. musicPlayerShowBusy();
  2111. const r = findNextAlbumCover(player.querySelector('.playlist .playing').dataset.albumUrl);
  2112. if (r === false) {
  2113. // Find next album in playlist
  2114. let reachedPlaying = false;
  2115. let found = false;
  2116. const lis = player.querySelectorAll('.playlist li');
  2117. for (let i = 0; i < lis.length; i++) {
  2118. if (reachedPlaying && lis[i].classList.contains('playlistheading')) {
  2119. lis[i].click();
  2120. found = true;
  2121. break;
  2122. } else if (lis[i].classList.contains('playing')) {
  2123. reachedPlaying = true;
  2124. }
  2125. }
  2126. if (!found) {
  2127. audio.play().then(_ => musicPlayerUpdatePositionState());
  2128. window.alert('End of playlist reached');
  2129. }
  2130. }
  2131. }, 10);
  2132. }
  2133. function musicPlayerToggleShuffle() {
  2134. player.querySelector('.shufflebutton').classList.toggle('active');
  2135. if (player.querySelector('.shufflebutton').classList.contains('active')) {
  2136. if (!window.confirm('Would you like to shuffle all albums on this page?\n\n(It may take several minutes to load all albums into the playlist)')) {
  2137. return;
  2138. }
  2139.  
  2140. // Load all albums from page into the player
  2141. addAllAlbumsAsHeadings();
  2142.  
  2143. // Load unloaded items in playlist
  2144. let delay = 0;
  2145. // Disable permanent storage for speed
  2146. storeTralbumDataPermanentlySwitch = false;
  2147. let n = player.querySelectorAll('.playlist .playlistheading a.notloaded').length + 1;
  2148. if (n > 0) {
  2149. const queueLoadingIndicator = document.body.appendChild(document.createElement('div'));
  2150. queueLoadingIndicator.setAttribute('id', 'queueloadingindicator');
  2151. queueLoadingIndicator.style = 'position:fixed;top:1%;left:10px;background:#d5dce4;color:#033162;font-size:10pt;border:1px solid #033162;z-index:200;';
  2152. }
  2153. const updateLoadingIndicator = function () {
  2154. const div = document.getElementById('queueloadingindicator');
  2155. if (div) {
  2156. div.innerHTML = `Loading albums into playlist. ${--n} albums remaining...`;
  2157. if (n <= 0) {
  2158. div.remove();
  2159. storeTralbumDataPermanentlySwitch = allFeatures.keepLibrary.enabled;
  2160. }
  2161. }
  2162. };
  2163. window.setTimeout(updateLoadingIndicator, 1);
  2164. player.querySelectorAll('.playlist .playlistheading a.notloaded').forEach(async function (notloaded) {
  2165. const url = notloaded.href;
  2166. notloaded.remove();
  2167. cachedTralbumData(url).then(function onCachedTralbumDataLoaded(TralbumData) {
  2168. if (TralbumData) {
  2169. addAlbumToPlaylist(TralbumData, null);
  2170. window.setTimeout(updateLoadingIndicator, 10);
  2171. } else {
  2172. // Delay to avoid rate limit
  2173. window.setTimeout(() => playAlbumFromUrl(url, null).then(updateLoadingIndicator), delay * 1000);
  2174. delay += 4;
  2175. }
  2176. });
  2177. });
  2178. }
  2179. }
  2180. function musicPlayerOnTimelineClick(ev) {
  2181. musicPlayerMovePlayHead(ev);
  2182. const timelineWidth = timeline.offsetWidth - playhead.offsetWidth;
  2183. const clickPercent = (ev.clientX - timeline.getBoundingClientRect().left) / timelineWidth;
  2184. audio.currentTime = currentDuration * clickPercent;
  2185. }
  2186. function musicPlayerOnTimeUpdate() {
  2187. const playpause = player.querySelector('.playpause');
  2188. const timelineWidth = timeline.offsetWidth - playhead.offsetWidth;
  2189. const playPercent = timelineWidth * (audio.currentTime / currentDuration);
  2190. playhead.style.marginLeft = playPercent + 'px';
  2191. if (audio.currentTime === currentDuration) {
  2192. playpause.querySelector('.play').style.display = 'none';
  2193. playpause.querySelector('.busy').style.display = '';
  2194. playpause.querySelector('.pause').style.display = 'none';
  2195. if ('mediaSession' in navigator) {
  2196. navigator.mediaSession.playbackState = 'none';
  2197. }
  2198. } else if (audio.paused) {
  2199. playpause.querySelector('.play').style.display = '';
  2200. playpause.querySelector('.busy').style.display = 'none';
  2201. playpause.querySelector('.pause').style.display = 'none';
  2202. if (document.title.startsWith('\u25B6\uFE0E ')) {
  2203. document.title = document.title.substring(3);
  2204. }
  2205. if ('mediaSession' in navigator) {
  2206. navigator.mediaSession.playbackState = 'paused';
  2207. }
  2208. } else {
  2209. playpause.querySelector('.play').style.display = 'none';
  2210. playpause.querySelector('.busy').style.display = 'none';
  2211. playpause.querySelector('.pause').style.display = '';
  2212. if (!document.title.startsWith('\u25B6\uFE0E ')) {
  2213. document.title = '\u25B6\uFE0E ' + document.title;
  2214. }
  2215. if ('mediaSession' in navigator) {
  2216. navigator.mediaSession.playbackState = 'playing';
  2217. }
  2218. }
  2219. player.querySelector('.durationDisplay .current').innerHTML = humanDuration(audio.currentTime);
  2220. }
  2221. function musicPlayerUpdateBufferBar() {
  2222. if (currentDuration) {
  2223. if (audio.buffered.length > 0) {
  2224. bufferbar.style.width = Math.min(100, 1 + parseInt(100 * audio.buffered.end(0) / currentDuration)) + '%';
  2225. } else {
  2226. bufferbar.style.width = '100%';
  2227. }
  2228. } else {
  2229. bufferbar.style.width = '0px';
  2230. }
  2231. }
  2232. function musicPlayerShowBusy(ev) {
  2233. const playpause = player.querySelector('.playpause');
  2234. playpause.querySelector('.play').style.display = 'none';
  2235. playpause.querySelector('.busy').style.display = '';
  2236. playpause.querySelector('.pause').style.display = 'none';
  2237. }
  2238. function musicPlayerMovePlayHead(event) {
  2239. const newMargLeft = event.clientX - timeline.getBoundingClientRect().left;
  2240. const timelineWidth = timeline.offsetWidth - playhead.offsetWidth;
  2241. if (newMargLeft >= 0 && newMargLeft <= timelineWidth) {
  2242. playhead.style.marginLeft = newMargLeft + 'px';
  2243. }
  2244. if (newMargLeft < 0) {
  2245. playhead.style.marginLeft = '0px';
  2246. }
  2247. if (newMargLeft > timelineWidth) {
  2248. playhead.style.marginLeft = timelineWidth + 'px';
  2249. }
  2250. }
  2251. function musicPlayerOnPlayheadMouseDown() {
  2252. onPlayHead = true;
  2253. window.addEventListener('mousemove', musicPlayerMovePlayHead, true);
  2254. audio.removeEventListener('timeupdate', musicPlayerOnTimeUpdate, false);
  2255. }
  2256. function musicPlayerOnPlayheadMouseUp(event) {
  2257. if (onPlayHead) {
  2258. musicPlayerMovePlayHead(event);
  2259. window.removeEventListener('mousemove', musicPlayerMovePlayHead, true);
  2260. // change current time
  2261. const timelineWidth = timeline.offsetWidth - playhead.offsetWidth;
  2262. const clickPercent = (event.clientX - timeline.getBoundingClientRect().left) / timelineWidth;
  2263. audio.currentTime = currentDuration * clickPercent;
  2264. audio.addEventListener('timeupdate', musicPlayerOnTimeUpdate, false);
  2265. }
  2266. onPlayHead = false;
  2267. }
  2268. function musicPlayerOnVolumeClick(ev) {
  2269. const volSlider = player.querySelector('.vol-slider');
  2270. const sliderWidth = volSlider.offsetWidth;
  2271. const percent = (ev.clientX - volSlider.getBoundingClientRect().left) / sliderWidth;
  2272. audio.logVolume = percent > 0.9 ? 1.0 : percent;
  2273. GM.setValue('volume', audio.logVolume);
  2274. }
  2275. function musicPlayerOnVolumeWheel(ev) {
  2276. ev.preventDefault();
  2277. const direction = Math.min(Math.max(-1.0, ev.deltaY), 1.0);
  2278. audio.logVolume = Math.min(Math.max(0.0, audio.logVolume - 0.05 * direction), 1.0);
  2279. GM.setValue('volume', audio.logVolume);
  2280. }
  2281. function musicPlayerOnMuteClick(ev) {
  2282. if (audio.logVolume < 0.01) {
  2283. if ('lastvolume' in audio.dataset && audio.dataset.lastvolume) {
  2284. audio.logVolume = audio.dataset.lastvolume;
  2285. GM.setValue('volume', audio.logVolume);
  2286. } else {
  2287. audio.logVolume = 1.0;
  2288. }
  2289. } else {
  2290. audio.dataset.lastvolume = audio.logVolume;
  2291. audio.logVolume = 0.0;
  2292. }
  2293. }
  2294. function musicPlayerOnVolumeChanged(ev) {
  2295. let icons;
  2296. if (NOEMOJI) {
  2297. const muteIcon = `<img style="width:20px" src="${speakerIconMuteSrc}" alt="\uD83D\uDD07">`;
  2298. const lowIcon = `<img style="width:20px" src="${speakerIconLowSrc}" alt="\uD83D\uDD07">`;
  2299. const middleIcon = `<img style="width:20px" src="${speakerIconMiddleSrc}" alt="\uD83D\uDD07">`;
  2300. const highIcon = `<img style="width:20px" src="${speakerIconHighSrc}" alt="\uD83D\uDD07">`;
  2301. icons = [muteIcon, lowIcon, middleIcon, highIcon];
  2302. } else {
  2303. icons = ['\uD83D\uDD07', '\uD83D\uDD08', '\uD83D\uDD09', '\uD83D\uDD0A'];
  2304. }
  2305. const percent = audio.logVolume;
  2306. const volSlider = player.querySelector('.vol-slider');
  2307. volSlider.querySelector('.vol-amt').style.width = parseInt(100 * percent) + '%';
  2308. const volIconWrapper = player.querySelector('.vol-icon-wrapper');
  2309. volIconWrapper.title = 'Mute (' + parseInt(percent * 100) + '%)';
  2310. if (percent < 0.05) {
  2311. volIconWrapper.innerHTML = icons[0];
  2312. } else if (percent < 0.3) {
  2313. volIconWrapper.innerHTML = icons[1];
  2314. } else if (percent < 0.8) {
  2315. volIconWrapper.innerHTML = icons[2];
  2316. } else {
  2317. volIconWrapper.innerHTML = icons[3];
  2318. }
  2319. }
  2320. function musicPlayerOnEnded(ev) {
  2321. musicPlayerNextSong();
  2322. window.setTimeout(() => player.querySelector('.playlist .playing').scrollIntoView({
  2323. block: 'nearest'
  2324. }), 200);
  2325. }
  2326. function musicPlayerOnPlaylistClick(ev, contextMenuRoot) {
  2327. const li = this;
  2328. if (ev.ctrlKey && player.querySelector('.playlist .isselected')) {
  2329. // Select multiple with ctrlKey
  2330. ev.preventDefault();
  2331. musicPlayerContextMenuCtrl.call(li, ev);
  2332. return;
  2333. }
  2334. if (ev.shiftKey && musicPlayerContextMenuLastSelectedLi && musicPlayerContextMenuLastSelectedLi.classList.contains('isselected')) {
  2335. // Select multiple with shift key
  2336. ev.preventDefault();
  2337. if (musicPlayerContextMenuShift.call(li, ev)) {
  2338. return;
  2339. }
  2340. }
  2341. musicPlayerNextSong(li);
  2342. if (contextMenuRoot) {
  2343. contextMenuRoot.remove();
  2344. }
  2345. }
  2346. function removeSelectedFromPlaylist(ev, contextMenuRoot) {
  2347. player.querySelectorAll('.playlist .isselected').forEach(function (li) {
  2348. if (li.classList.contains('playlistentry')) {
  2349. let walk = li.previousElementSibling;
  2350. let remainingTrackN = 0;
  2351. while (walk.classList.contains('playlistentry')) {
  2352. remainingTrackN++;
  2353. walk = walk.previousElementSibling;
  2354. }
  2355. walk = li.nextElementSibling;
  2356. while (walk.classList.contains('playlistentry')) {
  2357. remainingTrackN++;
  2358. walk = walk.nextElementSibling;
  2359. }
  2360. if (remainingTrackN === 0) {
  2361. // If this is last song of album, then remove also album
  2362. walk = li.previousElementSibling;
  2363. while (walk) {
  2364. if (walk.classList.contains('playlistheading')) {
  2365. walk.remove();
  2366. break;
  2367. }
  2368. walk = walk.previousElementSibling;
  2369. }
  2370. }
  2371. // Remove track
  2372. li.remove();
  2373. } else {
  2374. // Remove album
  2375. let next = li.nextElementSibling;
  2376. while (next && next.classList.contains('playlistentry')) {
  2377. next.remove();
  2378. next = li.nextElementSibling;
  2379. }
  2380. li.remove();
  2381. }
  2382. });
  2383. if (contextMenuRoot) {
  2384. contextMenuRoot.remove();
  2385. }
  2386. }
  2387. function musicPlayerOnPlaylistHeadingClick(ev, contextMenuRoot) {
  2388. const li = this;
  2389. const a = li.querySelector('a[href]');
  2390. if (a && a.classList.contains('notloaded')) {
  2391. const url = a.href;
  2392. cachedTralbumData(url).then(function onCachedTralbumDataLoaded(TralbumData) {
  2393. li.remove();
  2394. if (TralbumData) {
  2395. addAlbumToPlaylist(TralbumData);
  2396. } else {
  2397. playAlbumFromUrl(url);
  2398. }
  2399. });
  2400. } else if (a && li.nextElementSibling) {
  2401. li.nextElementSibling.click();
  2402. }
  2403. if (contextMenuRoot) {
  2404. contextMenuRoot.remove();
  2405. }
  2406. }
  2407. let musicPlayerContextMenuLastSelectedLi = null;
  2408. function musicPlayerContextMenuCtrl(ev) {
  2409. const li = this;
  2410. li.classList.toggle('isselected');
  2411. if (li.classList.contains('isselected')) {
  2412. musicPlayerContextMenuLastSelectedLi = li;
  2413. }
  2414. }
  2415. function musicPlayerContextMenuShift(ev) {
  2416. const li = this;
  2417. // Find the last selected element (i.e. in which direction we need to go)
  2418. let dir = 0;
  2419. let walk = li.previousElementSibling;
  2420. while (walk && dir === 0) {
  2421. if (walk === musicPlayerContextMenuLastSelectedLi) {
  2422. dir = -1;
  2423. }
  2424. walk = walk.previousElementSibling;
  2425. }
  2426. walk = li.nextElementSibling;
  2427. while (walk && dir === 0) {
  2428. if (walk === musicPlayerContextMenuLastSelectedLi) {
  2429. dir = 1;
  2430. break;
  2431. }
  2432. walk = walk.nextElementSibling;
  2433. }
  2434. // Select every track in-between
  2435. if (dir === -1) {
  2436. walk = li.previousElementSibling;
  2437. while (walk !== musicPlayerContextMenuLastSelectedLi) {
  2438. if (walk.classList.contains('playlistentry')) {
  2439. walk.classList.add('isselected');
  2440. }
  2441. walk = walk.previousElementSibling;
  2442. }
  2443. li.classList.add('isselected');
  2444. return true;
  2445. } else if (dir === 1) {
  2446. walk = li.nextElementSibling;
  2447. while (walk !== musicPlayerContextMenuLastSelectedLi) {
  2448. if (walk.classList.contains('playlistentry')) {
  2449. walk.classList.add('isselected');
  2450. }
  2451. walk = walk.nextElementSibling;
  2452. }
  2453. li.classList.add('isselected');
  2454. return true;
  2455. } else {
  2456. return false;
  2457. }
  2458. }
  2459. function musicPlayerContextMenu(ev) {
  2460. const li = this;
  2461. if (ev.ctrlKey && player.querySelector('.playlist .isselected')) {
  2462. // Select multiple with ctrl key
  2463. musicPlayerContextMenuCtrl.call(li, ev);
  2464. return;
  2465. }
  2466. if (ev.shiftKey && musicPlayerContextMenuLastSelectedLi && musicPlayerContextMenuLastSelectedLi.classList.contains('isselected')) {
  2467. // Select multiple with shift key
  2468. if (musicPlayerContextMenuShift.call(li, ev)) {
  2469. return;
  2470. }
  2471. }
  2472. player.querySelectorAll('.playlist .isselected').forEach(e => e.classList.remove('isselected'));
  2473. const oldMenu = document.getElementById('discographyplayer_contextmenu');
  2474. if (oldMenu) {
  2475. removeViaQuerySelector('#discographyplayer_contextmenu');
  2476. if (li.dataset.id && li.dataset.id === oldMenu.dataset.id) {
  2477. return;
  2478. }
  2479. }
  2480. li.classList.add('isselected');
  2481. musicPlayerContextMenuLastSelectedLi = li;
  2482. const div = document.body.appendChild(document.createElement('div'));
  2483. li.dataset.id = Math.random();
  2484. div.dataset.id = li.dataset.id;
  2485. div.setAttribute('id', 'discographyplayer_contextmenu');
  2486. div.style.left = ev.pageX + 11 + 'px';
  2487. div.style.top = ev.pageY + 'px';
  2488. const menuEntries = [];
  2489. if (li.classList.contains('playlistentry') || li.classList.contains('playlistheading')) {
  2490. menuEntries.push(['Remove selected', 'Remove selected tracks or albums from playlist\nSelect more with CTRL + Right click', removeSelectedFromPlaylist]);
  2491. }
  2492. if (li.classList.contains('playlistentry')) {
  2493. menuEntries.push(['Play track', 'Start playback', musicPlayerOnPlaylistClick]);
  2494. }
  2495. if (li.classList.contains('playlistheading')) {
  2496. menuEntries.push(['Play album', 'Start playback', musicPlayerOnPlaylistHeadingClick]);
  2497. }
  2498. menuEntries.forEach(function (menuEntry) {
  2499. const subMenu = div.appendChild(document.createElement('div'));
  2500. subMenu.classList.add('contextmenu_submenu');
  2501. subMenu.appendChild(document.createTextNode(menuEntry[0]));
  2502. subMenu.setAttribute('title', menuEntry[1]);
  2503. subMenu.addEventListener('click', function (clickEvent) {
  2504. menuEntry[2].call(li, clickEvent, div);
  2505. });
  2506. });
  2507. }
  2508. function musicPlayerOnPlaylistContextMenu(ev) {
  2509. ev.preventDefault();
  2510. musicPlayerContextMenu.call(this, ev);
  2511. }
  2512. function musicPlayerOnPlaylistHeadingContextMenu(ev) {
  2513. ev.preventDefault();
  2514. musicPlayerContextMenu.call(this, ev);
  2515. }
  2516. function musicPlayerFavicon(url) {
  2517. removeViaQuerySelector(document.head, 'link[rel*=icon]');
  2518. const link = document.createElement('link');
  2519. link.type = 'image/x-icon';
  2520. link.rel = 'shortcut icon';
  2521. link.href = url;
  2522. document.head.appendChild(link);
  2523. }
  2524. function musicPlayerCollectWishlistClick(ev) {
  2525. ev.preventDefault();
  2526. if (player.querySelector('.collect-wishlist').dataset === 'own') {
  2527. return;
  2528. }
  2529. let url = player.querySelector('.collect-wishlist').dataset.albumUrl;
  2530. if (this.classList.contains('track') && player.querySelector('.collect-wishlist').dataset.trackUrl) {
  2531. // Wishlist track
  2532. url = player.querySelector('.collect-wishlist').dataset.trackUrl;
  2533. }
  2534. player.querySelectorAll('.collect-wishlist>*').forEach(function (e) {
  2535. e.style.display = 'none';
  2536. });
  2537. window.open(url + '#collect-wishlist');
  2538. }
  2539. async function musicPlayerCollectListenedClick(ev) {
  2540. ev.preventDefault();
  2541. const collectListened = player.querySelector('.collect-listened');
  2542. const url = collectListened.dataset.albumUrl;
  2543. window.setTimeout(function musicPlayerCollectListenedResetTimeout() {
  2544. player.querySelectorAll('.collect-listened>*').forEach(function (e) {
  2545. e.style.display = 'none';
  2546. });
  2547. player.querySelector('.collect-listened .listened-saving').style.display = 'inline-block';
  2548. player.querySelector('.collect-listened').style.cursor = 'wait';
  2549. }, 0);
  2550. let albumData = await myAlbumsGetAlbum(url);
  2551. if (!albumData) {
  2552. albumData = await myAlbumsNewFromUrl(url, {});
  2553. }
  2554. if (albumData.listened) {
  2555. albumData.listened = false;
  2556. } else {
  2557. albumData.listened = new Date().toJSON();
  2558. }
  2559. collectListened.dataset.listened = albumData.listened;
  2560. await myAlbumsUpdateAlbum(albumData);
  2561. player.querySelectorAll('.collect-listened>*').forEach(function (e) {
  2562. e.style.display = 'none';
  2563. });
  2564. if (albumData.listened) {
  2565. player.querySelector('.collect-listened .listened').style.display = 'inline-block';
  2566. } else {
  2567. player.querySelector('.collect-listened .mark-listened').style.display = 'inline-block';
  2568. }
  2569. player.querySelector('.collect-listened').style.cursor = '';
  2570. window.setTimeout(makeAlbumLinksGreat, 100);
  2571. }
  2572. function musicPlayerUpdatePositionState() {
  2573. if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
  2574. navigator.mediaSession.setPositionState({
  2575. duration: audio.duration || currentDuration || 180,
  2576. playbackRate: audio.playbackRate,
  2577. position: audio.currentTime
  2578. });
  2579. }
  2580. }
  2581. function musicPlayerCookieChannel(onStopEventCb) {
  2582. if (!BANDCAMPDOMAIN) {
  2583. return;
  2584. }
  2585. window.addEventListener('message', function onMessage(event) {
  2586. // Receive messages from the cookie channel event handler
  2587. if (event.origin === document.location.protocol + '//' + document.location.hostname && event.data && typeof event.data === 'object' && 'discographyplayerCookiechannelPlaylist' in event.data && event.data.discographyplayerCookiechannelPlaylist.length >= 2 && event.data.discographyplayerCookiechannelPlaylist[1] === 'stop') {
  2588. onStopEventCb(event.data.discographyplayerCookiechannelPlaylist);
  2589. }
  2590. });
  2591. const script = document.createElement('script');
  2592. script.innerHTML = `
  2593. if(typeof Cookie !== 'undefined') {
  2594. var channel = new Cookie.CommChannel('playlist')
  2595. channel.send('stop')
  2596. channel.subscribe(function(a,b) {
  2597. window.postMessage({'discographyplayerCookiechannelPlaylist': b}, document.location.href)
  2598. })
  2599. channel.startListening()
  2600. window.addEventListener('message', function onMessage (event) {
  2601. // Receive messages from the user script
  2602. if (event.origin === document.location.protocol + '//' + document.location.hostname
  2603. && event.data && typeof(event.data) === 'object' && 'discographyplayerCookiechannelPlaylist' in event.data
  2604. && event.data.discographyplayerCookiechannelPlaylist === 'sendstop') {
  2605. channel.send('stop')
  2606. }
  2607. })
  2608. window.addEventListener('unload', function(event) {
  2609. channel.cleanup()
  2610. })
  2611. }
  2612. `;
  2613. document.head.appendChild(script);
  2614. }
  2615. function musicPlayerCookieChannelSendStop(onStopEventCb) {
  2616. if (BANDCAMPDOMAIN) {
  2617. window.postMessage({
  2618. discographyplayerCookiechannelPlaylist: 'sendstop'
  2619. }, document.location.href);
  2620. }
  2621. }
  2622. function musicPlayerSaveState() {
  2623. // Add remaining albums as headings
  2624. addAllAlbumsAsHeadings();
  2625. // Remove context menu and selection, we don't want to restore those
  2626. player.querySelectorAll('.playlist .isselected').forEach(e => e.classList.remove('isselected'));
  2627. removeViaQuerySelector('#discographyplayer_contextmenu');
  2628. let startPlaybackIndex = false;
  2629. const playlistEntries = player.querySelectorAll('.playlist .playlistentry');
  2630. for (let i = 0; i < playlistEntries.length; i++) {
  2631. if (playlistEntries[i].classList.contains('playing')) {
  2632. startPlaybackIndex = i;
  2633. break;
  2634. }
  2635. }
  2636. const startPlaybackTime = audio.currentTime;
  2637. return GM.setValue('musicPlayerState', JSON.stringify({
  2638. time: new Date().getTime(),
  2639. htmlPlaylist: player.querySelector('.playlist').innerHTML,
  2640. startPlayback: !audio.paused,
  2641. startPlaybackIndex,
  2642. startPlaybackTime,
  2643. shuffleActive: player.querySelector('.shufflebutton').classList.contains('active')
  2644. }));
  2645. }
  2646. function musicPlayerRestoreState(state) {
  2647. if (!allFeatures.discographyplayerPersist.enabled) {
  2648. return;
  2649. }
  2650. if (state.time + 1000 * 30 < new Date().getTime()) {
  2651. // Saved state expires after 30 seconds
  2652. return;
  2653. }
  2654.  
  2655. // Re-create music player
  2656. musicPlayerCreate();
  2657. player.querySelector('.playlist').innerHTML = state.htmlPlaylist;
  2658. const playlistEntries = player.querySelectorAll('.playlist .playlistentry');
  2659. playlistEntries.forEach(function addPlaylistEntryOnClick(li) {
  2660. li.addEventListener('click', musicPlayerOnPlaylistClick);
  2661. li.addEventListener('contextmenu', musicPlayerOnPlaylistContextMenu);
  2662. });
  2663. player.querySelectorAll('.playlist .playlistheading').forEach(function addPlaylistHeadingEntryOnClick(li) {
  2664. li.addEventListener('click', musicPlayerOnPlaylistHeadingClick);
  2665. li.addEventListener('contextmenu', musicPlayerOnPlaylistHeadingContextMenu);
  2666. });
  2667. if (state.startPlaybackIndex !== false) {
  2668. player.querySelectorAll('.playlist .playing').forEach(function (el) {
  2669. el.classList.remove('playing');
  2670. });
  2671. playlistEntries[state.startPlaybackIndex].classList.add('playing');
  2672. window.setTimeout(() => player.querySelector('.playlist .playing').scrollIntoView({
  2673. block: 'nearest'
  2674. }), 200);
  2675. }
  2676. // Start playback
  2677. if (state.startPlayback && state.startPlaybackIndex !== false) {
  2678. musicPlayerPlaySong(playlistEntries[state.startPlaybackIndex], state.startPlaybackTime);
  2679. }
  2680. if ('shuffleActive' in state && state.shuffleActive) {
  2681. player.querySelector('.shufflebutton').classList.add('active');
  2682. }
  2683. }
  2684. function musicPlayerToggleMinimize(ev, hide) {
  2685. if (hide || player.style.bottom !== '-57px') {
  2686. player.style.bottom = '-57px';
  2687. this.classList.add('minimized');
  2688. } else {
  2689. player.style.bottom = '0px';
  2690. this.classList.remove('minimized');
  2691. }
  2692. }
  2693. function musicPlayerPlaylistFullHeight() {
  2694. // Extend the playlist to the full height of the window
  2695. if ('mode' in this.dataset && this.dataset.mode === 'full_height') {
  2696. // Already in full height mode
  2697. return;
  2698. }
  2699. // Store width so it does not change on multiple mouse-overs
  2700. this.dataset.mode = 'full_height';
  2701. let width = this.clientWidth;
  2702. if ('width' in this.dataset) {
  2703. width = this.dataset.width;
  2704. } else {
  2705. this.dataset.width = width;
  2706. }
  2707. // Set CSS to full height
  2708. this.style.position = 'fixed';
  2709. this.style.maxHeight = '100%';
  2710. this.style.height = '100%';
  2711. this.style.maxWidth = `${width}px`;
  2712. this.style.width = `${width}px`;
  2713. this.style.top = '0px';
  2714. }
  2715. function musicPlayerPlaylistNormalHeight() {
  2716. // Revert the playlist to the normal height of the discography player
  2717. if ('mode' in this.dataset && this.dataset.mode !== 'full_height') {
  2718. // Already in normal height mode
  2719. return;
  2720. }
  2721. if (document.getElementById('discographyplayer_contextmenu')) {
  2722. // Context menu is open, don't change the height
  2723. return;
  2724. }
  2725. this.dataset.mode = 'normal';
  2726.  
  2727. // Revert CSS
  2728. this.style.position = '';
  2729. this.style.maxHeight = '';
  2730. this.style.maxWidth = '';
  2731. this.style.top = '';
  2732. }
  2733. function musicPlayerClose() {
  2734. if (player) {
  2735. player.style.display = 'none';
  2736. }
  2737. if (audio) {
  2738. audio.pause();
  2739. }
  2740. document.querySelectorAll('img.albumIsCurrentlyPlaying').forEach(img => img.classList.remove('albumIsCurrentlyPlaying'));
  2741. document.querySelectorAll('.albumIsCurrentlyPlayingIndicator').forEach(div => div.remove());
  2742. }
  2743. function musicPlayerCreate() {
  2744. if (player) {
  2745. player.style.display = 'block';
  2746. return;
  2747. }
  2748. musicPlayerCookieChannel(_ => musicPlayerStop());
  2749. const img1px = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOsmLZvJgAFwQJn5VVZ5QAAAABJRU5ErkJggg==';
  2750. const listenedListUrl = findUserProfileUrl() + '#listened-tab';
  2751. const checkSymbol = NOEMOJI ? '✓' : '✔';
  2752. player = document.createElement('div');
  2753. document.body.appendChild(player);
  2754. player.id = 'discographyplayer';
  2755. player.innerHTML = `
  2756. <div class="col col25 nowPlaying">
  2757. <div class="currentlyPlaying">
  2758. <a class="cover" target="_blank" href="#">
  2759. <img src="${img1px}">
  2760. </a>
  2761. <div class="info">
  2762. <a class="link" target="_blank" href="#">
  2763. <div class="title">◧◩◨▧■□▩</div>
  2764. <div class="artist">by <span>◩▧◧□ ◩◨▧ ■◩▩</span></div>
  2765. <div>from <span class="album">◨■■▩ ▧◨□</span></div>
  2766. </a>
  2767. </div>
  2768. </div>
  2769. <div class="nextInRow">
  2770. <a class="cover" target="_blank" href="#">
  2771. <img src="${img1px}">
  2772. </a>
  2773. <div class="info">
  2774. <a class="link" target="_blank" href="#">
  2775. <div class="title">◧◩◨▧■□▩</div>
  2776. <div>by <span class="artist">◩▧◧□ ◩◨▧ ■◩▩</span></div>
  2777. <div>from <span class="album">◨■■▩ ▧◨□</span></div>
  2778. </a>
  2779. </div>
  2780. </div>
  2781. </div>
  2782. <div class="col col25 colcontrols">
  2783. <audio autoplay="autoplay" preload="auto"></audio>
  2784. <div class="audioplayer">
  2785. <div id="timeline">
  2786. <div id="bufferbar" class="bufferbaranimation"></div>
  2787. <div id="playhead"></div>
  2788. </div>
  2789. <div class="controls">
  2790.  
  2791. <div class="prevalbum" title="Previous album">
  2792. <div class="arrowbutton prevalbum-icon"></div>
  2793. </div>
  2794.  
  2795. <div class="prev" title="Previous song">
  2796. <div class="arrowbutton prev-icon"></div>
  2797. </div>
  2798.  
  2799. <div class="playpause" title="Play/Pause">
  2800. <div class="play" style="display: none;"></div>
  2801. <div class="busy" style="display: none;"></div>
  2802. <div class="pause" style=""></div>
  2803. </div>
  2804.  
  2805. <div class="next" title="Next song">
  2806. <div class="arrowbutton next-icon"></div>
  2807. </div>
  2808.  
  2809. <div class="nextalbum" title="Next album">
  2810. <div class="arrowbutton nextalbum-icon"></div>
  2811. </div>
  2812.  
  2813. <div class="shuffleswitch" title="Shuffle">
  2814. <div class="shufflebutton" style="background-image:${spriteRepeatShuffle}"></div>
  2815. </div>
  2816.  
  2817. </div>
  2818. <div class="durationDisplay"><span class="current">-</span>/<span class="total">-</span></div>
  2819.  
  2820. <a class="downloadlink" title="Download mp3">
  2821. </a>
  2822. <br class="clb">
  2823. </div>
  2824. </div>
  2825. <div class="col col35">
  2826. <ol class="playlist"></ol>
  2827. </div>
  2828. <div class="col col15 colcontrols colvolumecontrols">
  2829.  
  2830. <div class="vol">
  2831. <div class="vol-icon-wrapper" title="Mute">
  2832. 🔊
  2833. </div>
  2834. <div class="vol-slider">
  2835. <div class="vol-amt" style="width: 100%;"></div>
  2836. <div class="vol-bg"></div>
  2837. </div>
  2838. </div>
  2839.  
  2840. <div class="collect">
  2841. <div class="collect-wishlist">
  2842. <a class="wishlist-default" href="https://bandcamp.com/wishlist">Wishlist</a>
  2843.  
  2844. <span class="wishlist-add">
  2845. <span class="bc-ui2 icon add-item-icon"></span>
  2846. <span class="add-item-label track" title="Add this song to your wishlist">Add song</span>
  2847. <span class="slash">/</span>
  2848. <span class="add-item-label album" title="Add this album to your wishlist">Add album to wishlist</span>
  2849. </span>
  2850. <span class="wishlist-collected">
  2851. <span class="bc-ui2 icon collected-item-icon"></span>
  2852. <span>In Wishlist</span>
  2853. </span>
  2854. <span class="wishlist-own" title="You own this album">
  2855. <span class="bc-ui2 icon own-item-icon"></span>
  2856. <span>You own this</span>
  2857. </span>
  2858. <span class="wishlist-saving">
  2859. Saving....
  2860. </span>
  2861. </div>
  2862. <div class="collect-listened">
  2863. <a class="listened-default" href="${listenedListUrl}">
  2864. Played albums
  2865. </a>
  2866. <span class="listened" title="Mark album as NOT played">
  2867. <span class="listened-symbol">${checkSymbol}</span>
  2868. <span class="listened-label">Played</span>
  2869. </span>
  2870. <span class="mark-listened" title="Mark album as played">
  2871. <span class="mark-listened-symbol">${checkSymbol}</span>
  2872. <span class="mark-listened-label">Mark as played</span>
  2873. </span>
  2874. <span class="listened-saving">
  2875. Saving...
  2876. </span>
  2877. </div>
  2878. </div>
  2879.  
  2880. <br class="cll">
  2881. <div class="minimizebutton">
  2882. <span class="minimized" title="Maximize player">&uarr;</span>
  2883. <span class="maximized" title="Minimize player">&darr;</span>
  2884. </div>
  2885. <div class="closebutton" title="Close player">x</div>
  2886. </div>`;
  2887. addStyle(discographyplayerCSS);
  2888. if (allFeatures.discographyplayerSidebar.enabled) {
  2889. // Sidebar discographyplayer
  2890. addStyle(discographyplayerSidebarCSS);
  2891. }
  2892. audio = player.querySelector('audio');
  2893. addLogVolume(audio);
  2894. getStoredVolume(function setVolumeCallback(volume) {
  2895. audio.logVolume = volume;
  2896. });
  2897. playhead = player.querySelector('#playhead');
  2898. bufferbar = player.querySelector('#bufferbar');
  2899. timeline = player.querySelector('#timeline');
  2900. player.querySelector('.minimizebutton').addEventListener('click', musicPlayerToggleMinimize);
  2901. player.querySelector('.closebutton').addEventListener('click', musicPlayerClose);
  2902. audio.addEventListener('ended', musicPlayerOnEnded);
  2903. audio.addEventListener('timeupdate', musicPlayerOnTimeUpdate);
  2904. audio.addEventListener('volumechange', musicPlayerOnVolumeChanged);
  2905. audio.addEventListener('canplaythrough', function onCanPlayThrough() {
  2906. currentDuration = audio.duration;
  2907. player.querySelector('.durationDisplay .total').innerHTML = humanDuration(currentDuration);
  2908. });
  2909. timeline.addEventListener('click', musicPlayerOnTimelineClick, false);
  2910. playhead.addEventListener('mousedown', musicPlayerOnPlayheadMouseDown, false);
  2911. window.addEventListener('mouseup', musicPlayerOnPlayheadMouseUp, false);
  2912. player.querySelector('.prevalbum').addEventListener('click', musicPlayerPrevAlbum);
  2913. player.querySelector('.prev').addEventListener('click', musicPlayerPrev);
  2914. player.querySelector('.playpause').addEventListener('click', musicPlayerPlay);
  2915. player.querySelector('.next').addEventListener('click', musicPlayerNext);
  2916. player.querySelector('.nextalbum').addEventListener('click', musicPlayerNextAlbum);
  2917. player.querySelector('.shuffleswitch').addEventListener('click', musicPlayerToggleShuffle);
  2918. player.querySelector('.vol-slider').addEventListener('click', musicPlayerOnVolumeClick);
  2919. player.querySelector('.vol').addEventListener('wheel', musicPlayerOnVolumeWheel, {
  2920. passive: false
  2921. });
  2922. player.querySelector('.vol-icon-wrapper').addEventListener('click', musicPlayerOnMuteClick);
  2923. player.querySelector('.collect-wishlist .track').addEventListener('click', musicPlayerCollectWishlistClick);
  2924. player.querySelector('.collect-wishlist .album').addEventListener('click', musicPlayerCollectWishlistClick);
  2925. player.querySelector('.collect-listened').addEventListener('click', musicPlayerCollectListenedClick);
  2926. player.querySelector('.downloadlink').addEventListener('click', function onDownloadLinkClick(ev) {
  2927. const addSpinner = el => el.classList.add('downloading');
  2928. const removeSpinner = el => el.classList.remove('downloading');
  2929. downloadMp3FromLink(ev, this, addSpinner, removeSpinner);
  2930. });
  2931. if (allFeatures.discographyplayerFullHeightPlaylist.enabled && !allFeatures.discographyplayerSidebar.enabled) {
  2932. player.querySelector('.playlist').addEventListener('mouseover', musicPlayerPlaylistFullHeight);
  2933. player.querySelector('.playlist').addEventListener('mouseout', musicPlayerPlaylistNormalHeight);
  2934. }
  2935. if (NOEMOJI) {
  2936. player.querySelector('.downloadlink').innerHTML = '↓';
  2937. }
  2938. window.addEventListener('unload', function onPageUnLoad(ev) {
  2939. if (allFeatures.discographyplayerPersist.enabled && player.style.display !== 'none' && !audio.paused) {
  2940. musicPlayerSaveState();
  2941. }
  2942. });
  2943. window.setInterval(musicPlayerUpdateBufferBar, 1200);
  2944. }
  2945. function addHeadingToPlaylist(title, url, albumLoaded) {
  2946. musicPlayerCreate();
  2947. let content = document.createTextNode('💽 ' + title);
  2948. if (url) {
  2949. const a = document.createElement('a');
  2950. a.href = url;
  2951. a.target = '_blank';
  2952. a.appendChild(content);
  2953. content = a;
  2954. a.className = albumLoaded ? 'loaded' : 'notloaded';
  2955. a.title = 'Open album page';
  2956. }
  2957. const li = document.createElement('li');
  2958. li.appendChild(content);
  2959. li.className = 'playlistheading';
  2960. if (!albumLoaded) {
  2961. li.className += ' notloaded';
  2962. li.title = 'Load album into playlist';
  2963. }
  2964. li.addEventListener('click', musicPlayerOnPlaylistHeadingClick);
  2965. li.addEventListener('contextmenu', musicPlayerOnPlaylistHeadingContextMenu);
  2966. player.querySelector('.playlist').appendChild(li);
  2967. }
  2968. function addToPlaylist(startPlayback, data) {
  2969. musicPlayerCreate();
  2970. const li = document.createElement('li');
  2971. if (data.trackNumber != null && data.trackNumber !== 'null') {
  2972. li.appendChild(document.createTextNode((data.trackNumber > 9 ? '' : '0') + data.trackNumber + '. ' + data.artist + ' - ' + data.title));
  2973. } else {
  2974. li.appendChild(document.createTextNode(data.artist + ' - ' + data.title));
  2975. }
  2976. const span = document.createElement('span');
  2977. span.className = 'duration';
  2978. span.appendChild(document.createTextNode(humanDuration(data.duration)));
  2979. li.appendChild(span);
  2980. li.value = data.trackNumber;
  2981. li.dataset.file = data.file;
  2982. li.dataset.title = data.title;
  2983. li.dataset.trackNumber = data.trackNumber;
  2984. li.dataset.duration = data.duration;
  2985. li.dataset.artist = data.artist;
  2986. li.dataset.album = data.album;
  2987. li.dataset.albumUrl = data.albumUrl;
  2988. li.dataset.albumCover = data.albumCover;
  2989. li.dataset.inWishlist = data.inWishlist;
  2990. li.dataset.isPurchased = data.isPurchased;
  2991. li.dataset.isDownloadable = data.isDownloadable;
  2992. li.dataset.trackUrl = data.trackUrl;
  2993. li.addEventListener('click', musicPlayerOnPlaylistClick);
  2994. li.addEventListener('contextmenu', musicPlayerOnPlaylistContextMenu);
  2995. li.className = 'playlistentry';
  2996. player.querySelector('.playlist').appendChild(li);
  2997. if (startPlayback) {
  2998. player.querySelectorAll('.playlist .playing').forEach(function (el) {
  2999. el.classList.remove('playing');
  3000. });
  3001. li.classList.add('playing');
  3002. musicPlayerPlaySong(li);
  3003. window.setTimeout(() => player.querySelector('.playlist .playing').scrollIntoView({
  3004. block: 'nearest'
  3005. }), 200);
  3006. }
  3007. }
  3008. function addAlbumToPlaylist(TralbumData, startPlaybackIndex = 0) {
  3009. let i = 0;
  3010. const artist = TralbumData.artist;
  3011. const album = TralbumData.current.title;
  3012. const albumUrl = document.location.protocol + '//' + albumKey(TralbumData.url);
  3013. const albumCover = `https://f4.bcbits.com/img/a${TralbumData.art_id}_2.jpg`;
  3014. addHeadingToPlaylist(album, 'url' in TralbumData ? TralbumData.url : false, true);
  3015. let streamable = 0;
  3016. for (const key in TralbumData.trackinfo) {
  3017. const track = TralbumData.trackinfo[key];
  3018. if (!track.file) {
  3019. continue;
  3020. }
  3021. const trackNumber = track.track_num;
  3022. const file = track.file[Object.keys(track.file)[0]];
  3023. const title = track.title;
  3024. const duration = track.duration;
  3025. const trackUrl = track.title_link;
  3026. const inWishlist = 'tralbum_collect_info' in TralbumData && 'is_collected' in TralbumData.tralbum_collect_info && TralbumData.tralbum_collect_info.is_collected;
  3027. const isDownloadable = track.is_downloadable === true;
  3028. const isPurchased = 'tralbum_collect_info' in TralbumData && 'is_purchased' in TralbumData.tralbum_collect_info && TralbumData.tralbum_collect_info.is_purchased;
  3029. addToPlaylist(startPlaybackIndex === i++, {
  3030. file,
  3031. title,
  3032. trackNumber,
  3033. trackUrl,
  3034. duration,
  3035. artist,
  3036. album,
  3037. albumUrl,
  3038. albumCover,
  3039. inWishlist,
  3040. isDownloadable,
  3041. isPurchased
  3042. });
  3043. streamable++;
  3044. }
  3045. if (streamable === 0) {
  3046. const li = document.createElement('li');
  3047. li.appendChild(document.createTextNode((NOEMOJI ? '\u27C1' : '\uD83D\uDE22') + ' Album is not streamable'));
  3048. player.querySelector('.playlist').appendChild(li);
  3049. }
  3050. player.querySelectorAll('.playlist .playlistheading a.notloaded').forEach(function (el) {
  3051. // Move unloaded items to the end
  3052. el.parentNode.parentNode.appendChild(el.parentNode);
  3053. });
  3054. }
  3055. function addAllAlbumsAsHeadings() {
  3056. const as = document.querySelectorAll('.music-grid .music-grid-item a[href*="/album/"],.music-grid .music-grid-item a[href*="/track/"]');
  3057. const lis = player.querySelectorAll('.playlist .playlistentry');
  3058. const unloadedAs = player.querySelectorAll('.playlist .playlistheading.notloaded a');
  3059. const isAlreadyInPlaylist = function (url) {
  3060. for (let i = 0; i < lis.length; i++) {
  3061. if (albumKey(lis[i].dataset.albumUrl) === albumKey(url)) {
  3062. return true;
  3063. }
  3064. }
  3065. for (let i = 0; i < unloadedAs.length; i++) {
  3066. if (albumKey(unloadedAs[i].href) === albumKey(url)) {
  3067. return true;
  3068. }
  3069. }
  3070. return false;
  3071. };
  3072. for (let i = 0; i < as.length; i++) {
  3073. const url = as[i].href;
  3074. // Check if already in playlist
  3075. if (!isAlreadyInPlaylist(url)) {
  3076. const title = ('textContent' in as[i].dataset ? as[i].dataset.textContent : as[i].querySelector('.title').textContent).trim();
  3077. addHeadingToPlaylist(title, url, false);
  3078. }
  3079. }
  3080. }
  3081. let getTralbumDataDelay = 0;
  3082. function getTralbumData(url, retry = true) {
  3083. return new Promise(function getTralbumDataPromise(resolve, reject) {
  3084. GM.xmlHttpRequest({
  3085. method: 'GET',
  3086. url,
  3087. onload: function getTralbumDataOnLoad(response) {
  3088. if (!response.responseText || response.responseText.indexOf('400 Bad Request') !== -1) {
  3089. let msg = '';
  3090. try {
  3091. msg = response.responseText.split('<center>')[1].split('</center>')[0];
  3092. } catch (e) {
  3093. msg = response.responseText;
  3094. }
  3095. window.alert('An error occured. Please clear your cookies of bandcamp.com and try again.\n\nOriginal error:\n' + msg);
  3096. reject(new Error('Too many cookies'));
  3097. return;
  3098. }
  3099. if (!response.responseText || response.responseText.indexOf('429 Too Many Requests') !== -1) {
  3100. if (retry) {
  3101. retry = false;
  3102. getTralbumDataDelay += 3;
  3103. const delay = getTralbumDataDelay;
  3104. console.warn(`getTralbumData(): 429 Too Many Requests. Trying again in ${delay} seconds`);
  3105. window.setTimeout(() => getTralbumDataPromise(resolve, reject), delay * 1000);
  3106. return;
  3107. }
  3108. let msg = '';
  3109. try {
  3110. msg = response.responseText.split('<center>')[1].split('</center>')[0];
  3111. } catch (e) {
  3112. msg = response.responseText;
  3113. }
  3114. window.alert('An error occured. You\'re probably being rate limited by bandcamp.\n\nOriginal error:\n' + msg);
  3115. reject(new Error('429 Too Many Requests'));
  3116. return;
  3117. }
  3118. let TralbumData = null;
  3119. try {
  3120. if (response.responseText.indexOf('var TralbumData =') !== -1) {
  3121. TralbumData = JSON5.parse(response.responseText.split('var TralbumData =')[1].split('\n};\n')[0].replace(/"\s+\+\s+"/, '') + '\n}');
  3122. } else if (response.responseText.indexOf('data-tralbum="') !== -1) {
  3123. const str = decodeHTMLentities(response.responseText.split('data-tralbum="')[1].split('"')[0]);
  3124. TralbumData = JSON.parse(str);
  3125. if (retry && TralbumData && 'url' in TralbumData && Object.keys(TralbumData).length === 1) {
  3126. retry = false;
  3127. // Discography page -> try to get first album
  3128. console.debug('getTralbumDataPromise(), Not a album page, try to find first album');
  3129. const firstAlbumM = response.responseText.split('id="music-grid"')[1].match(/<a.*?href="(.*?(album|track)\/.+?)"/);
  3130. if (firstAlbumM && firstAlbumM[1]) {
  3131. let firstAlbumUrl = firstAlbumM[1];
  3132. if (!firstAlbumUrl.startsWith('http')) {
  3133. const hostname = new window.URL(response.finalUrl).hostname;
  3134. if (firstAlbumUrl.startsWith('/')) {
  3135. firstAlbumUrl = `https://${hostname}${firstAlbumUrl}`;
  3136. } else {
  3137. firstAlbumUrl = `https://${hostname}/${firstAlbumUrl}`;
  3138. }
  3139. }
  3140. if (url !== firstAlbumUrl) {
  3141. url = firstAlbumUrl;
  3142. console.debug('getTralbumDataPromise(), Not a album page, new url=', url);
  3143. window.setTimeout(() => getTralbumDataPromise(resolve, reject), 500);
  3144. return;
  3145. }
  3146. }
  3147. }
  3148.  
  3149. // Try to add tralbum_collect_info / TralbumCollectInfo
  3150. if (TralbumData && response.responseText.indexOf('data-tralbum-collect-info="') !== -1) {
  3151. const collectInfoStr = decodeHTMLentities(response.responseText.split('data-tralbum-collect-info="')[1].split('"')[0]);
  3152. TralbumData.tralbum_collect_info = JSON.parse(collectInfoStr);
  3153. }
  3154. }
  3155. } catch (e) {
  3156. window.alert('An error occured when parsing TralbumData from url=' + url + '.\n\nOriginal error:\n' + e);
  3157. reject(e);
  3158. return;
  3159. }
  3160. if (TralbumData) {
  3161. correctTralbumData(TralbumData, response.responseText);
  3162. resolve(TralbumData);
  3163. } else {
  3164. const msg = 'Could not parse TralbumData from url=' + url;
  3165. window.alert(msg);
  3166. console.error(response.responseText);
  3167. reject(new Error(msg));
  3168. }
  3169. },
  3170. onerror: function getTralbumDataOnError(response) {
  3171. console.error('getTralbumData(' + url + ') in onerror() Error: ' + response.status + '\nResponse:\n' + response.responseText + '\n' + ('error' in response ? response.error : ''));
  3172. reject(new Error('error' in response ? response.error : 'getTralbumData failed with GM.xmlHttpRequest.onerror'));
  3173. }
  3174. });
  3175. });
  3176. }
  3177. function correctTralbumData(TralbumDataObj, html) {
  3178. const TralbumData = JSON.parse(JSON.stringify(TralbumDataObj));
  3179. // Corrections for single tracks
  3180. if (TralbumData.current.type === 'track' && TralbumData.current.title.toLowerCase().indexOf('single') === -1) {
  3181. TralbumData.current.title += ' - Single';
  3182. }
  3183. for (let i = 0; i < TralbumData.trackinfo.length; i++) {
  3184. if (TralbumData.trackinfo[i].track_num === null) {
  3185. TralbumData.trackinfo[i].track_num = i + 1;
  3186. }
  3187. }
  3188. // Add tags from html
  3189. if (html && html.indexOf('tags-inline-label') !== -1) {
  3190. const m = html.split('tags-inline-label')[1].split('</div>')[0].match(/\/tag\/[^"]+"/g);
  3191. if (m && m.length > 0) {
  3192. TralbumData.tags = [];
  3193. m.forEach(function (t) {
  3194. t = t.split('/').pop();
  3195. t = t.substring(0, t.length - 1);
  3196. TralbumData.tags.push(t);
  3197. });
  3198. }
  3199. }
  3200. // Remove stuff we don't use to save storage space
  3201. delete TralbumData.current.require_email_0;
  3202. delete TralbumData.current.audit;
  3203. delete TralbumData.current.download_pref;
  3204. delete TralbumData.current.set_price;
  3205. delete TralbumData.current.killed;
  3206. delete TralbumData.current.auto_repriced;
  3207. delete TralbumData.current.minimum_price_nonzero;
  3208. delete TralbumData.current.minimum_price;
  3209. delete TralbumData.current.purchase_url;
  3210. delete TralbumData.current.new_desc_format;
  3211. delete TralbumData.current.private;
  3212. delete TralbumData.current.is_set_price;
  3213. delete TralbumData.current.require_email;
  3214. delete TralbumData.current.upc;
  3215. delete TralbumData.packages;
  3216. delete TralbumData.last_subscription_item;
  3217. delete TralbumData.last_subscription_item;
  3218. delete TralbumData.has_discounts;
  3219. delete TralbumData.is_bonus;
  3220. delete TralbumData.play_cap_data;
  3221. delete TralbumData.client_id_sig;
  3222. delete TralbumData.is_purchased;
  3223. delete TralbumData.items_purchased;
  3224. delete TralbumData.is_private_stream;
  3225. delete TralbumData.is_band_member;
  3226. delete TralbumData.licensed_version_ids;
  3227. delete TralbumData.package_associated_license_id;
  3228. for (let i = 0; i < TralbumData.trackinfo.length; i++) {
  3229. delete TralbumData.trackinfo[i].is_draft;
  3230. delete TralbumData.trackinfo[i].album_preorder;
  3231. delete TralbumData.trackinfo[i].unreleased_track;
  3232. delete TralbumData.trackinfo[i].encoding_error;
  3233. delete TralbumData.trackinfo[i].video_mobile_url;
  3234. delete TralbumData.trackinfo[i].encoding_pending;
  3235. delete TralbumData.trackinfo[i].video_poster_url;
  3236. delete TralbumData.trackinfo[i].video_source_type;
  3237. delete TralbumData.trackinfo[i].video_source_id;
  3238. delete TralbumData.trackinfo[i].video_mobile_url;
  3239. delete TralbumData.trackinfo[i].video_caption;
  3240. delete TralbumData.trackinfo[i].video_featured;
  3241. delete TralbumData.trackinfo[i].video_id;
  3242. for (const attr in TralbumData.trackinfo[i]) {
  3243. if (TralbumData.trackinfo[i][attr] === null) {
  3244. delete TralbumData.trackinfo[i][attr];
  3245. }
  3246. }
  3247. }
  3248. for (const attr in TralbumData) {
  3249. if (TralbumData[attr] === null) {
  3250. delete TralbumData[attr];
  3251. }
  3252. }
  3253. return TralbumData;
  3254. }
  3255. function albumKey(url) {
  3256. if (url.startsWith('/')) {
  3257. url = document.location.hostname + url;
  3258. }
  3259. if (url.indexOf('://') !== -1) {
  3260. url = url.split('://')[1];
  3261. }
  3262. if (url.indexOf('#') !== -1) {
  3263. url = url.split('#')[0];
  3264. }
  3265. if (url.indexOf('?') !== -1) {
  3266. url = url.split('?')[0];
  3267. }
  3268. return url;
  3269. }
  3270. function albumPath(url) {
  3271. if (url.startsWith('/')) {
  3272. return albumKey(url);
  3273. }
  3274. const a = document.createElement('a');
  3275. a.href = url;
  3276. return a.pathname;
  3277. }
  3278. async function cacheSet(gmKey, expires, key, value) {
  3279. const cache = JSON.parse(await GM.getValue(gmKey, '{}'));
  3280. const now = new Date().getTime();
  3281. for (const prop in cache) {
  3282. // Delete cached values, that are older than `expires`
  3283. if (now - new Date(cache[prop].time).getTime() > expires) {
  3284. delete cache[prop];
  3285. }
  3286. }
  3287. const data = {
  3288. value,
  3289. time: new Date().toJSON()
  3290. };
  3291. cache[key] = data;
  3292. await GM.setValue(gmKey, JSON.stringify(cache));
  3293. }
  3294. async function cacheGet(gmKey, expires, key, defaultsTo = null) {
  3295. const cache = JSON.parse(await GM.getValue(gmKey, '{}'));
  3296. const now = new Date().getTime();
  3297. for (const prop in cache) {
  3298. // Delete cached values, that are older than `expires`
  3299. if (now - new Date(cache[prop].time).getTime() > expires) {
  3300. delete cache[prop];
  3301. continue;
  3302. }
  3303. if (prop === key) {
  3304. return cache[prop].value;
  3305. }
  3306. }
  3307. return defaultsTo;
  3308. }
  3309. async function storeTralbumData(TralbumData) {
  3310. const expires = TRALBUM_CACHE_HOURS * ONEHOUR;
  3311. const cache = JSON.parse(await GM.getValue('tralbumdata', '{}'));
  3312. for (const prop in cache) {
  3313. // Delete cached values, that are older than 2 hours
  3314. if (new Date().getTime() - new Date(cache[prop].time).getTime() > expires) {
  3315. delete cache[prop];
  3316. }
  3317. }
  3318. TralbumData.time = new Date().toJSON();
  3319. cache[albumKey(TralbumData.url)] = TralbumData;
  3320. await GM.setValue('tralbumdata', JSON.stringify(cache));
  3321. storeTralbumDataPermanently(TralbumData);
  3322. }
  3323. async function cachedTralbumData(url) {
  3324. const expires = TRALBUM_CACHE_HOURS * ONEHOUR;
  3325. const key = albumKey(url);
  3326. const cache = JSON.parse(await GM.getValue('tralbumdata', '{}'));
  3327. for (const prop in cache) {
  3328. // Delete cached values, that are older than 2 hours
  3329. if (new Date().getTime() - new Date(cache[prop].time).getTime() > expires) {
  3330. delete cache[prop];
  3331. continue;
  3332. }
  3333. if (prop === key) {
  3334. return cache[prop];
  3335. }
  3336. }
  3337. return false;
  3338. }
  3339. async function storeTralbumDataPermanently(TralbumData) {
  3340. if (!storeTralbumDataPermanentlySwitch) {
  3341. return;
  3342. }
  3343. const library = JSON.parse(await GM.getValue('tralbumlibrary', '{}'));
  3344. const key = albumKey(TralbumData.url);
  3345. if (key in library) {
  3346. library[key] = Object.assign(library[key], TralbumData);
  3347. } else {
  3348. library[key] = TralbumData;
  3349. }
  3350. await GM.setValue('tralbumlibrary', JSON.stringify(library));
  3351. }
  3352. async function deletePermanentTralbum(url) {
  3353. const library = JSON.parse(await GM.getValue('tralbumlibrary', '{}'));
  3354. const key = albumKey(url);
  3355. if (key in library) {
  3356. delete library[key];
  3357. await GM.setValue('tralbumlibrary', JSON.stringify(library));
  3358. return key;
  3359. }
  3360. return null;
  3361. }
  3362. function playAlbumFromCover(ev, url) {
  3363. let parent = this;
  3364. if (!url) {
  3365. for (let j = 0; parent.tagName !== 'A' && j < 20; j++) {
  3366. parent = parent.parentNode;
  3367. }
  3368. url = parent.href;
  3369. }
  3370. parent.classList.add('discographyplayer_currentalbum');
  3371.  
  3372. // Check if already in playlist
  3373. if (player) {
  3374. musicPlayerCreate();
  3375. const lis = player.querySelectorAll('.playlist .playlistentry');
  3376. for (let i = 0; i < lis.length; i++) {
  3377. if (albumKey(lis[i].dataset.albumUrl) === albumKey(url)) {
  3378. lis[i].click();
  3379. return;
  3380. }
  3381. }
  3382. }
  3383.  
  3384. // Load data
  3385. cachedTralbumData(url).then(function onCachedTralbumDataLoaded(TralbumData) {
  3386. if (TralbumData) {
  3387. addAlbumToPlaylist(TralbumData);
  3388. } else {
  3389. playAlbumFromUrl(url);
  3390. }
  3391. });
  3392. }
  3393. function playAlbumFromUrl(url, startPlaybackIndex = 0) {
  3394. if (!url.startsWith('http')) {
  3395. url = document.location.protocol + '//' + url;
  3396. }
  3397. return getTralbumData(url).then(function onGetTralbumDataLoaded(TralbumData) {
  3398. storeTralbumData(TralbumData);
  3399. return addAlbumToPlaylist(TralbumData, startPlaybackIndex);
  3400. }).catch(function onGetTralbumDataError(e) {
  3401. window.alert('Could not play and load album data from url:\n' + url + '\n' + ('error' in e ? e.error : e));
  3402. console.error(e);
  3403. });
  3404. }
  3405. async function myAlbumsGetAlbum(url) {
  3406. const key = albumKey(url);
  3407. const data = JSON.parse(await GM.getValue('myalbums', '{}'));
  3408. if (key in data) {
  3409. return data[key];
  3410. } else {
  3411. return false;
  3412. }
  3413. }
  3414. async function myAlbumsUpdateAlbum(albumData) {
  3415. const key = albumKey(albumData.url);
  3416. const data = JSON.parse(await GM.getValue('myalbums', '{}'));
  3417. if (key in data) {
  3418. data[key] = Object.assign(data[key], albumData);
  3419. } else {
  3420. data[key] = albumData;
  3421. }
  3422. await GM.setValue('myalbums', JSON.stringify(data));
  3423. }
  3424. async function myAlbumsNewFromUrl(url, fallback) {
  3425. // Get data from cache or load from url
  3426. url = albumKey(url);
  3427. const albumData = fallback || {};
  3428. let TralbumData = await cachedTralbumData(url);
  3429. if (!TralbumData) {
  3430. try {
  3431. TralbumData = await getTralbumData(document.location.protocol + '//' + url);
  3432. } catch (e) {
  3433. console.error('myAlbumsNewFromUrl() Could not load album data from url:\n' + url);
  3434. }
  3435. if (TralbumData) {
  3436. storeTralbumData(TralbumData);
  3437. }
  3438. }
  3439. if (TralbumData) {
  3440. albumData.artist = TralbumData.artist;
  3441. albumData.title = TralbumData.current.title;
  3442. albumData.albumCover = `https://f4.bcbits.com/img/a${TralbumData.art_id}_2.jpg`;
  3443. albumData.releaseDate = TralbumData.current.release_date;
  3444. }
  3445. albumData.url = url;
  3446. albumData.listened = false;
  3447. return albumData;
  3448. }
  3449. function makeAlbumCoversGreat() {
  3450. if (!('makeAlbumCoversGreat' in document.head.dataset)) {
  3451. document.head.dataset.makeAlbumCoversGreat = true;
  3452. const campExplorerCSS = `
  3453. .music-grid-item {
  3454. position: relative
  3455. }
  3456. .music-grid-item .art-play {
  3457. margin-top: -50px;
  3458. }
  3459. `;
  3460. addStyle(`
  3461. .music-grid-item .art-play {
  3462. position: absolute;
  3463. width: 74px;
  3464. height: 54px;
  3465. left: 50%;
  3466. top: 50%;
  3467. margin-left: -36px;
  3468. margin-top: -27px;
  3469. opacity: 0;
  3470. transition: opacity 0.2s;
  3471. }
  3472. .music-grid-item .art-play-bg {
  3473. position: absolute;
  3474. width: 100%;
  3475. height: 100%;
  3476. left: 0;
  3477. top: 0;
  3478. background: #000;
  3479. border-radius: 4px;
  3480. }
  3481. .music-grid-item .art-play-icon {
  3482. position: absolute;
  3483. width: 0;
  3484. height: 0;
  3485. left: 28px;
  3486. top: 17px;
  3487. border-width: 10px 0 10px 17px;
  3488. border-color: transparent transparent transparent #fff;
  3489. border-style: dashed dashed dashed solid;
  3490. }
  3491. .music-grid-item:hover .art-play {
  3492. opacity: 0.6;
  3493. }
  3494.  
  3495. ${CAMPEXPLORER ? campExplorerCSS : ''}
  3496. `);
  3497. }
  3498. const onclick = function onclick(ev) {
  3499. ev.preventDefault();
  3500. playAlbumFromCover.apply(this, ev);
  3501. };
  3502. const artPlay = document.createElement('div');
  3503. artPlay.className = 'art-play';
  3504. artPlay.innerHTML = '<div class="art-play-bg"></div><div class="art-play-icon"></div>';
  3505. if (CAMPEXPLORER) {
  3506. document.querySelectorAll('ul.albums').forEach(e => e.classList.add('music-grid'));
  3507. document.querySelectorAll('ul.albums li.album').forEach(e => e.classList.add('music-grid-item'));
  3508. }
  3509.  
  3510. // Albums, single tracks, artists, label etc
  3511. const imgs = document.querySelectorAll('.music-grid .music-grid-item a[href] img');
  3512. for (let i = 0; i < imgs.length; i++) {
  3513. if (imgs[i].parentNode.getElementsByClassName('art-play').length) {
  3514. continue;
  3515. }
  3516. imgs[i].addEventListener('click', onclick);
  3517.  
  3518. // Add play overlay
  3519. const clone = artPlay.cloneNode(true);
  3520. clone.addEventListener('click', onclick);
  3521. imgs[i].parentNode.appendChild(clone);
  3522. }
  3523. }
  3524. function makeTagSearchCoversGreat() {
  3525. const onclick = function onclick(ev) {
  3526. ev.preventDefault();
  3527. const a = this.parentNode.querySelector('.info a[href]');
  3528. playAlbumFromCover.call(this, ev, a.href);
  3529. };
  3530. document.querySelectorAll('.dig-deeper-item').forEach(function (div) {
  3531. const artDiv = div.querySelector('div.art');
  3532. const dumbArtCopy = artDiv.cloneNode(true);
  3533. artDiv.parentNode.replaceChild(dumbArtCopy, artDiv);
  3534. dumbArtCopy.addEventListener('click', onclick);
  3535. });
  3536. }
  3537. async function makeAlbumLinksGreat(parentElement) {
  3538. const doc = parentElement || document;
  3539. const myalbums = JSON.parse(await GM.getValue('myalbums', '{}'));
  3540. if (!('makeAlbumLinksGreat' in document.head.dataset)) {
  3541. document.head.dataset.makeAlbumLinksGreat = true;
  3542. addStyle(`
  3543. .bdp_check_onlinkhover_container { z-index:1002; position:absolute; display:none }
  3544. .bdp_check_onlinkhover_container_shown { display:block; background-color:rgba(255,255,255,0.9); padding:0px 2px 0px 0px; border-radius:5px }
  3545. .bdp_check_onlinkhover_container:hover { position:absolute; transition: all 300ms linear; background-color:rgba(255,255,255,0.9); padding:0px 10px 0px 7px; border-radius:5px }
  3546. .bdp_check_onchecked_container { z-index:-1; position:absolute; opacity:0.0; margin-top:-2px}
  3547. a:hover .bdp_check_onchecked_container { z-index:1002; position:absolute; transition: opacity 300ms linear; opacity:1.0}
  3548.  
  3549. .bdp_check_onlinkhover_symbol {color:rgba(0,0,50,0.7)}
  3550. .bdp_check_onlinkhover_text {color:rgba(0,0,50,0.7)}
  3551. .bdp_check_onlinkhover_container:hover .bdp_check_onlinkhover_symbol { color:rgba(0,0,100,1.0) }
  3552. .bdp_check_onlinkhover_container:hover .bdp_check_onlinkhover_text { color:rgba(0,100,0,1.0)}
  3553. .bdp_check_onchecked_symbol { color:rgba(0,100,0,0.8) }
  3554. .bdp_check_onchecked_text { color:rgba(150,200,150,0.8) }
  3555.  
  3556. a:hover .bdp_check_onchecked_symbol { text-shadow: 1px 1px #fff; color:rgba(0,50,0,1.0); transition: all 300ms linear }
  3557. a:hover .bdp_check_onchecked_text { text-shadow: 1px 1px #000; color:rgba(200,255,200,0.8); transition: all 300ms linear }
  3558.  
  3559. `);
  3560. }
  3561. const excluded = [...document.querySelectorAll('#carousel-player .now-playing a')];
  3562. excluded.push(...document.querySelectorAll('#discographyplayer a'));
  3563. excluded.push(...document.querySelectorAll('#pastreleases a'));
  3564.  
  3565. /*
  3566. <div class="bdp_check_container bdp_check_onlinkhover_container"><span class="bdp_check_onlinkhover_symbol">\u2610</span> <span class="bdp_check_onlinkhover_text">Check</span></div>
  3567. <div class="bdp_check_container bdp_check_onlinkhover_container"><span class="bdp_check_onlinkhover_symbol">\u1f5f9</span> <span class="bdp_check_onlinkhover_text">Check</span></div>
  3568. <span class="bdp_check_onchecked_symbol">\u2611</span> TITLE <div class="bdp_check_container bdp_check_onchecked_container"><span class="bdp_check_onchecked_text">Played</span></div>
  3569. */
  3570.  
  3571. const onClickSetListened = async function onClickSetListenedAsync(ev) {
  3572. ev.preventDefault();
  3573. let parentA = this;
  3574. for (let j = 0; parentA.tagName !== 'A' && j < 20; j++) {
  3575. parentA = parentA.parentNode;
  3576. }
  3577. window.setTimeout(function showSavingLabel() {
  3578. parentA.style.cursor = 'wait';
  3579. parentA.querySelector('.bdp_check_container').innerHTML = 'Saving...';
  3580. }, 0);
  3581. const url = parentA.href;
  3582. let albumData = await myAlbumsGetAlbum(url);
  3583. if (!albumData) {
  3584. albumData = await myAlbumsNewFromUrl(url, {
  3585. title: this.dataset.textContent
  3586. });
  3587. }
  3588. albumData.listened = new Date().toJSON();
  3589. await myAlbumsUpdateAlbum(albumData);
  3590. window.setTimeout(function hideSavingLabel() {
  3591. parentA.style.cursor = '';
  3592. makeAlbumLinksGreat();
  3593. }, 100);
  3594. };
  3595. const onClickRemoveListened = async function onClickRemoveListenedAsync(ev) {
  3596. ev.preventDefault();
  3597. let parentA = this;
  3598. for (let j = 0; parentA.tagName !== 'A' && j < 20; j++) {
  3599. parentA = parentA.parentNode;
  3600. }
  3601. window.setTimeout(function showSavingLabel() {
  3602. parentA.style.cursor = 'wait';
  3603. parentA.querySelector('.bdp_check_container').innerHTML = 'Saving...';
  3604. }, 0);
  3605. const url = parentA.href;
  3606. const albumData = await myAlbumsGetAlbum(url);
  3607. if (albumData) {
  3608. albumData.listened = false;
  3609. await myAlbumsUpdateAlbum(albumData);
  3610. }
  3611. window.setTimeout(function hideSavingLabel() {
  3612. parentA.style.cursor = '';
  3613. makeAlbumLinksGreat();
  3614. }, 100);
  3615. };
  3616. const mouseOverLink = function onMouseOverLink(ev) {
  3617. const bdpCheckOnlinkhoverContainer = this.querySelector('.bdp_check_onlinkhover_container');
  3618. if (bdpCheckOnlinkhoverContainer) {
  3619. bdpCheckOnlinkhoverContainer.classList.add('bdp_check_onlinkhover_container_shown');
  3620. }
  3621. };
  3622. const mouseOutLink = function onMouseOutLink(ev) {
  3623. const a = this;
  3624. a.dataset.iv = window.setTimeout(function mouseOutLinkTimeout() {
  3625. const div = a.querySelector('.bdp_check_onlinkhover_container');
  3626. if (div) {
  3627. div.classList.remove('bdp_check_onlinkhover_container_shown');
  3628. div.dataset.iv = a.dataset.iv;
  3629. }
  3630. }, 1000);
  3631. };
  3632. const mouseMoveLink = function onMouseLoveLink(ev) {
  3633. if ('iv' in this.dataset) {
  3634. window.clearTimeout(this.dataset.iv);
  3635. }
  3636. };
  3637. const mouseOverDivCheck = function onMouseOverDivCheck(ev) {
  3638. const bdpCheckOnlinkhoverSymbol = this.querySelector('.bdp_check_onlinkhover_symbol');
  3639. if (bdpCheckOnlinkhoverSymbol) {
  3640. bdpCheckOnlinkhoverSymbol.innerText = NOEMOJI ? '\u2611' : '\uD83D\uDDF9';
  3641. }
  3642. if ('iv' in this.dataset) {
  3643. window.clearTimeout(this.dataset.iv);
  3644. }
  3645. };
  3646. const mouseOutDivCheck = function onMouseOutDivCheck(ev) {
  3647. const bdpCheckOnlinkhoverSymbol = this.querySelector('.bdp_check_onlinkhover_symbol');
  3648. if (bdpCheckOnlinkhoverSymbol) {
  3649. bdpCheckOnlinkhoverSymbol.innerText = '\u2610';
  3650. }
  3651. };
  3652. const divCheck = document.createElement('div');
  3653. divCheck.setAttribute('class', 'bdp_check_container bdp_check_onlinkhover_container');
  3654. divCheck.setAttribute('title', 'Mark as played');
  3655. divCheck.innerHTML = '<span class="bdp_check_onlinkhover_symbol">\u2610</span> <span class="bdp_check_onlinkhover_text">Check</span>';
  3656. const divChecked = document.createElement('div');
  3657. divChecked.setAttribute('class', 'bdp_check_container bdp_check_onchecked_container');
  3658. divChecked.innerHTML = '<span class="bdp_check_onchecked_text">Played</span>';
  3659. const spanChecked = document.createElement('span');
  3660. spanChecked.appendChild(document.createTextNode('\u2611 '));
  3661. spanChecked.setAttribute('class', 'bdp_check_onchecked_symbol');
  3662. const a = doc.querySelectorAll('a[href*="/album/"],.music-grid .music-grid-item a[href*="/track/"]');
  3663. let lastKey = '';
  3664. for (let i = 0; i < a.length; i++) {
  3665. if (excluded.indexOf(a[i]) !== -1) {
  3666. continue;
  3667. }
  3668. const key = albumKey(a[i].href);
  3669. if (key === lastKey) {
  3670. // Skip multiple consequent links to same album
  3671. continue;
  3672. }
  3673. const textContent = a[i].textContent.trim();
  3674. if (!textContent) {
  3675. // Skip album covers only
  3676. continue;
  3677. }
  3678. let div;
  3679. if (a[i].dataset.textContent) {
  3680. removeViaQuerySelector(a[i], '.bdp_check_onlinkhover_container');
  3681. removeViaQuerySelector(a[i], '.bdp_check_onchecked_container');
  3682. removeViaQuerySelector(a[i], '.bdp_check_onchecked_symbol');
  3683. } else {
  3684. a[i].dataset.textContent = textContent;
  3685. a[i].addEventListener('mouseover', mouseOverLink);
  3686. a[i].addEventListener('mousemove', mouseMoveLink);
  3687. a[i].addEventListener('mouseout', mouseOutLink);
  3688. }
  3689. if (key in myalbums && 'listened' in myalbums[key] && myalbums[key].listened) {
  3690. div = divChecked.cloneNode(true);
  3691. div.addEventListener('click', onClickRemoveListened);
  3692. const date = new Date(myalbums[key].listened);
  3693. const since = timeSince(date);
  3694. const dateStr = dateFormater(date);
  3695. div.title = since + ' ago\nClick to mark as NOT played';
  3696. div.querySelector('.bdp_check_onchecked_text').appendChild(document.createTextNode(' ' + dateStr));
  3697. const span = spanChecked.cloneNode(true);
  3698. span.title = since + ' ago\nClick to mark as NOT played';
  3699. span.addEventListener('click', onClickRemoveListened);
  3700. const firstText = firstChildWithText(a[i]) || a[i].firstChild;
  3701. firstText.parentNode.insertBefore(span, firstText);
  3702. } else {
  3703. div = divCheck.cloneNode(true);
  3704. div.addEventListener('mouseover', mouseOverDivCheck);
  3705. div.addEventListener('mouseout', mouseOutDivCheck);
  3706. div.addEventListener('click', onClickSetListened);
  3707. }
  3708. a[i].appendChild(div);
  3709. lastKey = key;
  3710. }
  3711. }
  3712. function removeTheTimeHasComeToOpenThyHeartWallet() {
  3713. if ('theTimeHasComeToOpenThyHeartWallet' in document.head.dataset) {
  3714. return;
  3715. }
  3716. document.head.dataset.theTimeHasComeToOpenThyHeartWallet = true;
  3717. document.head.appendChild(document.createElement('script')).innerHTML = `
  3718. Log.debug("theTimeHasComeToOpenThyHeartWallet: start...")
  3719. function removeViaQuerySelector (parent, selector) {
  3720. if (typeof selector === 'undefined') {
  3721. selector = parent
  3722. parent = document
  3723. }
  3724. for (let el = parent.querySelector(selector); el; el = parent.querySelector(selector)) {
  3725. el.remove()
  3726. }
  3727. }
  3728. if (typeof TralbumData !== 'undefined') {
  3729. if (TralbumData.play_cap_data) {
  3730. TralbumData.play_cap_data.streaming_limit = 100
  3731. TralbumData.play_cap_data.streaming_limits_enabled = false
  3732. }
  3733. for(let i = 0; i < TralbumData.trackinfo.length; i++) {
  3734. TralbumData.trackinfo[i].is_capped = false
  3735. TralbumData.trackinfo[i].play_count = 1
  3736. }
  3737.  
  3738. /* // Alternative would be create new player
  3739. TralbumLimits.onPlayerInit = () => true
  3740. TralbumLimits.updatePlayCounts = () => true
  3741. Player.init(TralbumData, AlbumPage.onPlayerInit);
  3742. */
  3743.  
  3744. // Update player with modified TralbumData
  3745. Player.update(TralbumData)
  3746. Log.debug("theTimeHasComeToOpenThyHeartWallet: player updated")
  3747. }
  3748.  
  3749. // Restore lyrics onClick
  3750. function parentByClassName(node, className) {
  3751. while(!node.parentNode.classList.contains(className)) {
  3752. node = node.parentNode
  3753. if (node.parentNode === document.documentElement) {
  3754. return null
  3755. }
  3756. }
  3757. return node.parentNode
  3758. }
  3759. /*
  3760. // seems this is no longer necessary
  3761. function onLyricsClick (ev) {
  3762. ev.preventDefault()
  3763. const tr = parentByClassName(this, 'track_row_view')
  3764. if (tr.classList.contains('current_track')) {
  3765. parentByClassName(tr, 'track_list').classList.toggle('auto_lyrics')
  3766. } else {
  3767. tr.classList.toggle('showlyrics')
  3768. }
  3769. }
  3770. document.querySelectorAll('#track_table .track_row_view .info_link a').forEach(function (a) {
  3771. a.addEventListener('click', onLyricsClick)
  3772. })
  3773. */
  3774.  
  3775. // Hide popup (not really needed, but won't hurt)
  3776. window.setInterval(function() {
  3777. if(document.getElementById('play-limits-dialog-cancel-btn')) {
  3778. document.getElementById('play-limits-dialog-cancel-btn').click()
  3779. window.setTimeout(function() {
  3780. removeViaQuerySelector(document, '.ui-dialog.ui-widget')
  3781. removeViaQuerySelector(document, '.ui-widget-overlay')
  3782. }, 100)
  3783. }
  3784. }, 3000)
  3785. Log.debug("theTimeHasComeToOpenThyHeartWallet: done!")
  3786. `;
  3787. }
  3788. function makeCarouselPlayerGreatAgain() {
  3789. if (player) {
  3790. // Hide/minimize discography player
  3791. const closePlayerOnCarouselIv = window.setInterval(function closePlayerOnCarouselInterval() {
  3792. if (!document.getElementById('carousel-player') || document.getElementById('carousel-player').getClientRects()[0].bottom - window.innerHeight > 0) {
  3793. return;
  3794. }
  3795. if (player.style.display === 'none') {
  3796. // Put carousel player back down in normal position, because discography player is hidden forever
  3797. document.getElementById('carousel-player').style.bottom = '0px';
  3798. window.clearInterval(closePlayerOnCarouselIv);
  3799. } else if (!player.style.bottom) {
  3800. // Minimize discography player and push carousel player up above the minimized player
  3801. musicPlayerToggleMinimize.call(player.querySelector('.minimizebutton'), null, true);
  3802. document.getElementById('carousel-player').style.bottom = player.clientHeight - 57 + 'px';
  3803. }
  3804. }, 5000);
  3805. }
  3806. let addListenedButtonToCarouselPlayerLast = null;
  3807. const addListenedButtonToCarouselPlayer = function listenedButtonOnCarouselPlayer() {
  3808. const url = document.querySelector('#carousel-player a[href]') ? albumKey(document.querySelector('#carousel-player a[href]').href) : null;
  3809. if (url && addListenedButtonToCarouselPlayerLast === url) {
  3810. return;
  3811. }
  3812. if (!url) {
  3813. console.error('No url found in carousel player: `#carousel-player a[href]`');
  3814. return;
  3815. }
  3816. addListenedButtonToCarouselPlayerLast = url;
  3817. removeViaQuerySelector('#carousel-player .carousellistenedstatus');
  3818. const a = document.createElement('a');
  3819. a.className = 'carousellistenedstatus';
  3820. a.addEventListener('click', ev => ev.preventDefault());
  3821. document.querySelector('#carousel-player .controls-extra').insertBefore(a, document.querySelector('#carousel-player .controls-extra').firstChild);
  3822. a.innerHTML = '<span class="listenedstatus">Loading...</span>';
  3823. a.href = 'https://' + url;
  3824. makeAlbumLinksGreat(a.parentNode).then(function () {
  3825. removeViaQuerySelector(a, '.listenedstatus');
  3826. const span = document.createElement('span');
  3827. span.addEventListener('click', function () {
  3828. const span = this;
  3829. span.parentNode.querySelector('.bdp_check_container').click();
  3830. window.setTimeout(function () {
  3831. if (span.parentNode.querySelector('.bdp_check_container').textContent.indexOf('Played') !== -1) {
  3832. span.parentNode.innerHTML = 'Listened';
  3833. } else {
  3834. span.parentNode.innerHTML = 'Unplayed';
  3835. }
  3836. }, 3000);
  3837. });
  3838. if (a.querySelector('.bdp_check_onchecked_text')) {
  3839. span.className = 'listenedstatus listened';
  3840. span.innerHTML = '<span class="listened-symbol">✓</span> <span class="listened-label">Played</span>';
  3841. } else {
  3842. span.className = 'listenedstatus mark-listened';
  3843. span.innerHTML = '<span class="mark-listened-symbol">✓</span> <span class="mark-listened-label">Mark as played</span>';
  3844. }
  3845. a.insertBefore(span, a.firstChild);
  3846. a.dataset.textContent = document.querySelector('#carousel-player .now-playing .info a .artist span').textContent + ' - ' + document.querySelector('#carousel-player .now-playing .info a .title').textContent;
  3847. });
  3848. };
  3849. let lastMediaHubMeta = [null, null];
  3850. const onNotificationClick = function () {
  3851. if (!document.querySelector('#carousel-player .transport .next-icon').classList.contains('disabled')) {
  3852. document.querySelector('#carousel-player .transport .next-icon').click();
  3853. }
  3854. };
  3855. const updateChromePositionState = function () {
  3856. const audio = document.querySelector('body>audio');
  3857. if (audio && 'mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
  3858. navigator.mediaSession.setPositionState({
  3859. duration: audio.duration || 180,
  3860. playbackRate: audio.playbackRate,
  3861. position: audio.currentTime
  3862. });
  3863. }
  3864. };
  3865. const addChromeMediaHubToCarouselPlayer = function chromeMediaHubToCarouselPlayer() {
  3866. const title = document.querySelector('#carousel-player .info-progress span[data-bind*="trackTitle"]').textContent.trim();
  3867. const artwork = document.querySelector('#carousel-player .now-playing img').src;
  3868. if (lastMediaHubMeta[0] === title && lastMediaHubMeta[1] === artwork) {
  3869. return;
  3870. }
  3871. lastMediaHubMeta = [title, artwork];
  3872. const artist = document.querySelector('#carousel-player .now-playing .artist span').textContent.trim();
  3873. const album = document.querySelector('#carousel-player .now-playing .title').textContent.trim();
  3874.  
  3875. // Notification
  3876. if (allFeatures.nextSongNotifications.enabled && 'notification' in GM) {
  3877. GM.notification({
  3878. title: document.location.host,
  3879. text: title + '\nby ' + artist + '\nfrom ' + album,
  3880. image: artwork,
  3881. highlight: false,
  3882. silent: true,
  3883. timeout: NOTIFICATION_TIMEOUT,
  3884. onclick: onNotificationClick
  3885. });
  3886. }
  3887.  
  3888. // Media hub
  3889. if ('mediaSession' in navigator) {
  3890. const audio = document.querySelector('body>audio');
  3891. if (audio) {
  3892. navigator.mediaSession.playbackState = !audio.paused ? 'playing' : 'paused';
  3893. updateChromePositionState();
  3894. }
  3895. navigator.mediaSession.metadata = new MediaMetadata({
  3896. title,
  3897. artist,
  3898. album,
  3899. artwork: [{
  3900. src: artwork,
  3901. sizes: '350x350',
  3902. type: 'image/jpeg'
  3903. }]
  3904. });
  3905. if (!document.querySelector('#carousel-player .transport .prev-icon').classList.contains('disabled')) {
  3906. navigator.mediaSession.setActionHandler('previoustrack', () => document.querySelector('#carousel-player .transport .prev-icon').click());
  3907. } else {
  3908. navigator.mediaSession.setActionHandler('previoustrack', null);
  3909. }
  3910. if (!document.querySelector('#carousel-player .transport .next-icon').classList.contains('disabled')) {
  3911. navigator.mediaSession.setActionHandler('nexttrack', () => document.querySelector('#carousel-player .transport .next-icon').click());
  3912. } else {
  3913. navigator.mediaSession.setActionHandler('nexttrack', null);
  3914. }
  3915. const playButton = document.querySelector('#carousel-player .playpause .play');
  3916. if (playButton && playButton.style.display === 'none') {
  3917. navigator.mediaSession.setActionHandler('play', null);
  3918. navigator.mediaSession.setActionHandler('pause', function () {
  3919. document.querySelector('#carousel-player .playpause').click();
  3920. navigator.mediaSession.playbackState = 'paused';
  3921. });
  3922. } else {
  3923. navigator.mediaSession.setActionHandler('play', function () {
  3924. document.querySelector('#carousel-player .playpause').click();
  3925. navigator.mediaSession.playbackState = 'playing';
  3926. });
  3927. navigator.mediaSession.setActionHandler('pause', null);
  3928. }
  3929. if (audio) {
  3930. navigator.mediaSession.setActionHandler('seekbackward', function (event) {
  3931. const skipTime = event.seekOffset || DEFAULTSKIPTIME;
  3932. audio.currentTime = Math.max(audio.currentTime - skipTime, 0);
  3933. updateChromePositionState();
  3934. });
  3935. navigator.mediaSession.setActionHandler('seekforward', function (event) {
  3936. const skipTime = event.seekOffset || DEFAULTSKIPTIME;
  3937. audio.currentTime = Math.min(audio.currentTime + skipTime, audio.duration);
  3938. updateChromePositionState();
  3939. });
  3940. try {
  3941. navigator.mediaSession.setActionHandler('stop', function () {
  3942. audio.pause();
  3943. audio.currentTime = 0;
  3944. navigator.mediaSession.playbackState = 'paused';
  3945. });
  3946. } catch (error) {
  3947. console.warn('Warning! The "stop" media session action is not supported.');
  3948. }
  3949. try {
  3950. navigator.mediaSession.setActionHandler('seekto', function (event) {
  3951. if (event.fastSeek && 'fastSeek' in audio) {
  3952. audio.fastSeek(event.seekTime);
  3953. return;
  3954. }
  3955. audio.currentTime = event.seekTime;
  3956. updateChromePositionState();
  3957. });
  3958. } catch (error) {
  3959. console.warn('Warning! The "seekto" media session action is not supported.');
  3960. }
  3961. }
  3962. }
  3963. };
  3964. window.setInterval(function addListenedButtonToCarouselPlayerInterval() {
  3965. if (!document.getElementById('carousel-player') || document.getElementById('carousel-player').getClientRects()[0].bottom - window.innerHeight > 0) {
  3966. return;
  3967. }
  3968. addListenedButtonToCarouselPlayer();
  3969. addChromeMediaHubToCarouselPlayer();
  3970. }, 2000);
  3971. addStyle(`
  3972. #carousel-player a.carousellistenedstatus:link,#carousel-player a.carousellistenedstatus:visited,#carousel-player a.carousellistenedstatus:hover{
  3973. text-decoration:none;
  3974. cursor:default
  3975. }
  3976. #carousel-player .listened .listened-symbol{
  3977. color:rgb(0,220,50);
  3978. text-shadow:1px 0px #DDD,-1px 0px #DDD,0px -1px #DDD,0px 1px #DDD
  3979. }
  3980. #carousel-player .mark-listened .mark-listened-symbol{
  3981. color:#FFF;
  3982. text-shadow:1px 0px #959595,-1px 0px #959595,0px -1px #959595,0px 1px #959595
  3983. }
  3984. #carousel-player .mark-listened:hover .mark-listened-symbol{
  3985. text-shadow:1px 0px #0AF,-1px 0px #0AF,0px -1px #0AF,0px 1px #0AF
  3986. }
  3987. `);
  3988. }
  3989. async function addListenedButtonToCollectControls() {
  3990. const lastLi = document.querySelector('.share-panel-wrapper-desktop ul li');
  3991. if (!lastLi) {
  3992. window.setTimeout(addListenedButtonToCollectControls, 300);
  3993. return;
  3994. }
  3995. const checkSymbol = NOEMOJI ? '✓' : '✔';
  3996. const myalbums = JSON.parse(await GM.getValue('myalbums', '{}'));
  3997. const key = albumKey(document.location.href);
  3998. const listened = key in myalbums && 'listened' in myalbums[key] && myalbums[key].listened;
  3999. const onClickSetListened = async function onClickSetListenedAsync(ev) {
  4000. ev.preventDefault();
  4001. let parent = this;
  4002. for (let j = 0; parent.tagName !== 'LI' && j < 20; j++) {
  4003. parent = parent.parentNode;
  4004. }
  4005. window.setTimeout(function showSavingLabel() {
  4006. parent.style.cursor = 'wait';
  4007. parent.innerHTML = 'Saving...';
  4008. }, 0);
  4009. const url = document.location.href;
  4010. let albumData = await myAlbumsGetAlbum(url);
  4011. if (!albumData) {
  4012. albumData = await myAlbumsNewFromUrl(url, {
  4013. title: this.dataset.textContent
  4014. });
  4015. }
  4016. albumData.listened = new Date().toJSON();
  4017. await myAlbumsUpdateAlbum(albumData);
  4018. window.setTimeout(addListenedButtonToCollectControls, 100);
  4019. };
  4020. const onClickRemoveListened = async function onClickRemoveListenedAsync(ev) {
  4021. ev.preventDefault();
  4022. let parent = this;
  4023. for (let j = 0; parent.tagName !== 'LI' && j < 20; j++) {
  4024. parent = parent.parentNode;
  4025. }
  4026. window.setTimeout(function showSavingLabel() {
  4027. parent.style.cursor = 'wait';
  4028. parent.innerHTML = 'Saving...';
  4029. }, 0);
  4030. const url = document.location.href;
  4031. const albumData = await myAlbumsGetAlbum(url);
  4032. if (albumData) {
  4033. albumData.listened = false;
  4034. await myAlbumsUpdateAlbum(albumData);
  4035. }
  4036. window.setTimeout(addListenedButtonToCollectControls, 100);
  4037. };
  4038. removeViaQuerySelector('#discographyplayer_sharepanel');
  4039. const li = lastLi.parentNode.appendChild(document.createElement('li'));
  4040. const button = li.appendChild(document.createElement('span'));
  4041. const icon = button.appendChild(document.createElement('span'));
  4042. const a = button.appendChild(document.createElement('a'));
  4043. li.setAttribute('id', 'discographyplayer_sharepanel');
  4044. a.addEventListener('click', ev => ev.preventDefault());
  4045. icon.className = 'sharepanelchecksymbol';
  4046. if (listened) {
  4047. const date = new Date(listened);
  4048. const since = timeSince(date);
  4049. button.title = since + '\nClick to mark as NOT played';
  4050. button.addEventListener('click', onClickRemoveListened);
  4051. icon.style.color = 'rgb(0,220,50)';
  4052. icon.style.textShadow = '1px 0px #DDD,-1px 0px #DDD,0px -1px #DDD,0px 1px #DDD';
  4053. icon.style.paddingRight = '5px';
  4054. icon.appendChild(document.createTextNode(checkSymbol));
  4055. a.appendChild(document.createTextNode('Played'));
  4056. li.appendChild(document.createTextNode(' - '));
  4057. const link = li.appendChild(document.createElement('span'));
  4058. const viewLink = link.appendChild(document.createElement('a'));
  4059. viewLink.href = findUserProfileUrl() + '#listened-tab';
  4060. viewLink.title = 'View list of played albums';
  4061. viewLink.appendChild(document.createTextNode('view'));
  4062. } else {
  4063. button.title = 'Click to mark as played';
  4064. button.addEventListener('click', onClickSetListened);
  4065. try {
  4066. icon.style.color = window.getComputedStyle(document.getElementById('pgBd')).backgroundColor;
  4067. icon.style.textShadow = '1px 0px #959595,-1px 0px #959595,0px -1px #959595,0px 1px #959595';
  4068. icon.style.paddingRight = '5px';
  4069. } catch (e) {
  4070. icon.style.color = '#959595';
  4071. icon.style.fontWeight = 700;
  4072. }
  4073. icon.appendChild(document.createTextNode(checkSymbol));
  4074. a.appendChild(document.createTextNode('Unplayed'));
  4075. }
  4076. }
  4077. function makeListenedListTabLink() {
  4078. const grid = document.getElementById('grids').appendChild(document.createElement('div'));
  4079. grid.className = 'grid';
  4080. grid.id = 'listened-grid';
  4081. const inner = grid.appendChild(document.createElement('div'));
  4082. inner.className = 'inner';
  4083. inner.innerHTML = 'Loading...';
  4084. const li = document.querySelector('ol#grid-tabs').appendChild(document.createElement('li'));
  4085. li.id = 'listenedlisttablink';
  4086. li.dataset.tab = 'listened';
  4087. li.setAttribute('data-grid-id', 'listened-grid');
  4088. const span = li.appendChild(document.createElement('span'));
  4089. span.className = 'tab-title';
  4090. span.appendChild(document.createTextNode('played'));
  4091. const count = span.appendChild(document.createElement('span'));
  4092. count.className = 'count';
  4093. GM.getValue('myalbums', '{}').then(function myalbumsLoaded(str) {
  4094. let n = 0;
  4095. const myalbums = JSON.parse(str);
  4096. for (const key in myalbums) {
  4097. if (myalbums[key].listened) {
  4098. n++;
  4099. }
  4100. }
  4101. count.appendChild(document.createTextNode(n));
  4102. });
  4103. li.addEventListener('click', showListenedListTab);
  4104. return li;
  4105. }
  4106. async function showListenedListTab() {
  4107. if (document.getElementById('owner-controls')) document.getElementById('owner-controls').style.display = 'none';
  4108. if (document.getElementById('wishlist-controls')) document.getElementById('wishlist-controls').style.display = 'none';
  4109. const grid = document.getElementById('listened-grid');
  4110. const gridActive = document.querySelector('#grids .grid.active');
  4111. if (gridActive && gridActive !== grid) {
  4112. gridActive.classList.remove('active');
  4113. }
  4114. grid.classList.add('active');
  4115. const tabLink = document.getElementById('listenedlisttablink');
  4116. const tabLinkActive = document.querySelector('#grid-tab li.active');
  4117. if (tabLinkActive && tabLinkActive !== tabLink) {
  4118. tabLinkActive.classList.remove('active');
  4119. }
  4120. tabLink.classList.add('active');
  4121. if (grid.querySelector('.collection-items')) {
  4122. return;
  4123. }
  4124. grid.innerHTML = '';
  4125. const collectionItems = grid.appendChild(document.createElement('div'));
  4126. collectionItems.className = 'collection-items';
  4127. const collectionGrid = collectionItems.appendChild(document.createElement('ol'));
  4128. collectionGrid.className = 'collection-grid';
  4129. const myalbums = JSON.parse(await GM.getValue('myalbums', '{}'));
  4130. for (const key in myalbums) {
  4131. const albumData = myalbums[key];
  4132. if (!albumData.listened) {
  4133. continue;
  4134. }
  4135. const artist = albumData.artist || 'Unkown artist';
  4136. const title = albumData.title || 'Unkown title';
  4137. const albumCover = albumData.albumCover || 'https://bandcamp.com/img/0.gif';
  4138. const url = key;
  4139. const date = new Date(albumData.listened);
  4140. const since = timeSince(date);
  4141. const dateStr = dateFormater(date);
  4142. let releaseDate;
  4143. if ('releaseDate' in albumData) {
  4144. releaseDate = dateFormaterRelease(new Date(albumData.releaseDate));
  4145. } else {
  4146. releaseDate = 'Unknown';
  4147. }
  4148. const li = collectionGrid.appendChild(document.createElement('li'));
  4149. li.className = 'collection-item-container';
  4150. li.innerHTML = `
  4151. <div class="collection-item-gallery-container">
  4152. <span class="bc-ui2 collect-item-icon-alt"></span>
  4153. <div class="collection-item-art-container">
  4154. <img class="collection-item-art" alt="" src="${albumCover}">
  4155. </div>
  4156. <div class="collection-title-details">
  4157. <a target="_blank" href="https://${url}" class="item-link">
  4158. <div class="collection-item-title">${title}</div>
  4159. <div class="collection-item-artist">by ${artist}</div>
  4160. </a>
  4161. </div>
  4162. <div class="collection-item-fav-track">
  4163. <span title="${since} ago" class="favoriteTrackLabel">played</span>
  4164. <div title="${since} ago">
  4165. <span class="fav-track-link">${dateStr}</span>
  4166. </div>
  4167. <span class="favoriteTrackLabel">released</span>
  4168. <div>
  4169. <span class="fav-track-link">${releaseDate}</span>
  4170. </div>
  4171. </div>
  4172. </div>
  4173. `;
  4174. }
  4175. }
  4176. function addVolumeBarToAlbumPage() {
  4177. // Do not add if one of these scripts already added a volume bar
  4178. // https://openuserjs.org/scripts/cuzi/Bandcamp_Volume_Bar
  4179. // https://openuserjs.org/scripts/Mranth0ny62/Bandcamp_Volume_Bar
  4180. // https://openuserjs.org/scripts/ArtificialInput/Bandcamp_Volume_Bar
  4181. // https://greasyfork.org/en/scripts/11047-bandcamp-volume-bar/
  4182. // https://greasyfork.org/en/scripts/38012-bandcamp-volume-bar/
  4183. if (document.querySelector('.volumeControl')) {
  4184. return false;
  4185. }
  4186. if (!document.querySelector('#trackInfoInner .playbutton')) {
  4187. return;
  4188. }
  4189. addStyle(`
  4190. /* Hide if inline_player is hidden */
  4191. .hidden .volumeButton,.hidden .volumeControl,.hidden .volumeLabel{
  4192. display:none
  4193. }
  4194.  
  4195. .volumeButton {
  4196. display: inline-block;
  4197. user-select:none;
  4198. background: #fff;
  4199. border: 1px solid #d9d9d9;
  4200. border-radius: 2px;
  4201. cursor: pointer;
  4202. min-height: 50px;
  4203. min-width: 54px;
  4204. text-align:center;
  4205. margin-top:5px;
  4206. }
  4207.  
  4208. .volumeSymbol {
  4209. margin-top: 16px;
  4210. font-size: 30px;
  4211. color:#222;
  4212. font-weight:bolder;
  4213. transform: rotate(-90deg);
  4214. text-shadow: rgb(255, 255, 255) 0px 0px 0px;
  4215. transition: text-shadow linear 300ms;
  4216. }
  4217. .volumeControl {
  4218. display:inline-block;
  4219. user-select:none;
  4220. top:5px;
  4221. }
  4222. .volumeLabel {
  4223. display:inline-block;
  4224. }
  4225.  
  4226. .nextsongcontrolbutton {
  4227. background:#fff;
  4228. border:1px solid #d9d9d9;
  4229. border-radius:2px;
  4230. cursor:pointer;
  4231. height:24px;
  4232. width:35px;
  4233. margin-top:2px;
  4234. margin-left:80px;
  4235. float:left;
  4236. text-align:center
  4237. }
  4238.  
  4239. .nextsongcontrolicon {
  4240. background-size:cover;
  4241. background-image:${spriteRepeatShuffle};
  4242. width:31px;
  4243. height:20px;
  4244. filter:drop-shadow(#FFF 1px 1px 2px);
  4245. display:inline-block;
  4246. margin-top:1px;
  4247. transition: filter 500ms;
  4248. }
  4249. .nextsongcontrolbutton.active .nextsongcontrolicon {
  4250. filter:drop-shadow(#0060F2 1px 1px 2px);
  4251. }
  4252.  
  4253. `);
  4254. const playbutton = document.querySelector('#trackInfoInner .playbutton');
  4255. const volumeButton = playbutton.cloneNode(true);
  4256. document.querySelector('#trackInfoInner .inline_player').appendChild(volumeButton);
  4257. volumeButton.classList.replace('playbutton', 'volumeButton');
  4258. volumeButton.style.width = playbutton.clientWidth + 'px';
  4259. const volumeSymbol = volumeButton.appendChild(document.createElement('div'));
  4260. volumeSymbol.className = 'volumeSymbol';
  4261. volumeSymbol.appendChild(document.createTextNode(CHROME ? '\uD83D\uDD5B' : '\u23F2'));
  4262. const progbar = document.querySelector('#trackInfoInner .progbar_cell .progbar');
  4263. const volumeBar = progbar.cloneNode(true);
  4264. document.querySelector('#trackInfoInner .inline_player').appendChild(volumeBar);
  4265. volumeBar.classList.add('volumeControl');
  4266. volumeBar.style.width = Math.max(200, progbar.clientWidth) + 'px';
  4267. const thumb = volumeBar.querySelector('.thumb');
  4268. thumb.setAttribute('id', 'deluxe_thumb');
  4269. const progbarFill = volumeBar.querySelector('.progbar_fill');
  4270. const volumeLabel = document.createElement('div');
  4271. document.querySelector('#trackInfoInner .inline_player').appendChild(volumeLabel);
  4272. volumeLabel.classList.add('volumeLabel');
  4273. let dragging = false;
  4274. let dragPos;
  4275. const width100 = volumeBar.clientWidth - (thumb.clientWidth + 2); // 2px border
  4276. const rot0 = CHROME ? -180 : -90;
  4277. const rot100 = CHROME ? 350 : 265 - rot0;
  4278. const blue0 = 180;
  4279. const blue100 = 75;
  4280. const green0 = 90;
  4281. const green100 = 100;
  4282. const audioAlbumPage = document.querySelector('body>audio');
  4283. addLogVolume(audioAlbumPage);
  4284. const volumeBarPos = volumeBar.getBoundingClientRect().left;
  4285. const displayVolume = function updateDisplayVolume() {
  4286. const level = audioAlbumPage.logVolume;
  4287. volumeLabel.innerHTML = parseInt(level * 100.0) + '%';
  4288. thumb.style.left = width100 * level + 'px';
  4289. progbarFill.style.width = parseInt(level * 100.0) + '%';
  4290. volumeSymbol.style.transform = 'rotate(' + (level * rot100 + rot0) + 'deg)';
  4291. if (level > 0.005) {
  4292. volumeSymbol.style.textShadow = 'rgb(0, ' + (level * green100 + green0) + ', ' + (level * blue100 + blue0) + ') 0px 0px 4px';
  4293. volumeSymbol.style.color = '#03a';
  4294. } else {
  4295. volumeSymbol.style.textShadow = 'rgb(255, 255, 255) 0px 0px 0px';
  4296. volumeSymbol.style.color = '#222';
  4297. }
  4298. };
  4299. thumb.addEventListener('mousedown', function thumbMouseDown(ev) {
  4300. if (ev.button === 0) {
  4301. dragging = true;
  4302. dragPos = ev.offsetX;
  4303. }
  4304. });
  4305. volumeBar.addEventListener('mouseup', function thumbMouseUp(ev) {
  4306. if (ev.button !== 0) {
  4307. return;
  4308. }
  4309. ev.preventDefault();
  4310. ev.stopPropagation();
  4311. if (!dragging) {
  4312. // Click on volume bar without dragging:
  4313. audioAlbumPage.muted = false;
  4314. audioAlbumPage.logVolume = Math.max(0.0, Math.min(1.0, (ev.pageX - volumeBarPos) / width100));
  4315. displayVolume();
  4316. }
  4317. dragging = false;
  4318. GM.setValue('volume', audioAlbumPage.logVolume);
  4319. });
  4320. document.addEventListener('mouseup', function documentMouseUp(ev) {
  4321. if (ev.button === 0 && dragging) {
  4322. dragging = false;
  4323. ev.preventDefault();
  4324. ev.stopPropagation();
  4325. GM.setValue('volume', audioAlbumPage.logVolume);
  4326. }
  4327. });
  4328. document.addEventListener('mousemove', function documentMouseMove(ev) {
  4329. if (ev.button === 0 && dragging) {
  4330. ev.preventDefault();
  4331. ev.stopPropagation();
  4332. audioAlbumPage.muted = false;
  4333. audioAlbumPage.logVolume = Math.max(0.0, Math.min(1.0, (ev.pageX - volumeBarPos - dragPos) / width100));
  4334. displayVolume();
  4335. }
  4336. });
  4337. const onWheel = function onMouseWheel(ev) {
  4338. ev.preventDefault();
  4339. const direction = Math.min(Math.max(-1.0, ev.deltaY), 1.0);
  4340. audioAlbumPage.logVolume = Math.min(Math.max(0.0, audioAlbumPage.logVolume - 0.05 * direction), 1.0);
  4341. displayVolume();
  4342. GM.setValue('volume', audioAlbumPage.logVolume);
  4343. };
  4344. volumeButton.addEventListener('wheel', onWheel, {
  4345. passive: false
  4346. });
  4347. volumeBar.addEventListener('wheel', onWheel, {
  4348. passive: false
  4349. });
  4350. volumeButton.addEventListener('click', function onVolumeButtonClick(ev) {
  4351. if (audioAlbumPage.logVolume < 0.01) {
  4352. if ('lastvolume' in audioAlbumPage.dataset && audioAlbumPage.dataset.lastvolume) {
  4353. audioAlbumPage.logVolume = audioAlbumPage.dataset.lastvolume;
  4354. GM.setValue('volume', audioAlbumPage.logVolume);
  4355. } else {
  4356. audioAlbumPage.logVolume = 1.0;
  4357. }
  4358. } else {
  4359. audioAlbumPage.dataset.lastvolume = audioAlbumPage.logVolume;
  4360. audioAlbumPage.logVolume = 0.0;
  4361. }
  4362. displayVolume();
  4363. });
  4364. displayVolume();
  4365. window.clearInterval(ivRestoreVolume);
  4366.  
  4367. // Repeat/shuffle buttons
  4368. const playnextcontrols = document.querySelector('#trackInfoInner .inline_player').appendChild(document.createElement('div'));
  4369.  
  4370. // Show repeat button
  4371. const repeatButton = playnextcontrols.appendChild(document.createElement('div'));
  4372. repeatButton.classList.add('nextsongcontrolbutton', 'repeat');
  4373. repeatButton.setAttribute('title', 'Repeat');
  4374. const repeatButtonIcon = repeatButton.appendChild(document.createElement('div'));
  4375. repeatButtonIcon.classList.add('nextsongcontrolicon');
  4376. repeatButton.dataset.repeat = 'none';
  4377. repeatButtonIcon.style.backgroundPositionY = '-20px';
  4378. repeatButton.addEventListener('click', function () {
  4379. const posY = this.getElementsByClassName('nextsongcontrolicon')[0].style.backgroundPositionY;
  4380. if (posY === '-20px') {
  4381. this.getElementsByClassName('nextsongcontrolicon')[0].style.backgroundPositionY = '-40px';
  4382. this.classList.toggle('active');
  4383. this.dataset.repeat = 'one';
  4384. } else if (posY === '-40px') {
  4385. this.getElementsByClassName('nextsongcontrolicon')[0].style.backgroundPositionY = '-60px';
  4386. this.dataset.repeat = 'all';
  4387. } else {
  4388. this.getElementsByClassName('nextsongcontrolicon')[0].style.backgroundPositionY = '-20px';
  4389. this.classList.toggle('active');
  4390. this.dataset.repeat = 'none';
  4391. }
  4392. });
  4393. if (allFeatures.albumPageAutoRepeatAll.enabled) {
  4394. repeatButton.click();
  4395. repeatButton.click();
  4396. }
  4397.  
  4398. // Show shuffle button
  4399. const shuffleButton = playnextcontrols.appendChild(document.createElement('div'));
  4400. if (document.querySelectorAll('#track_table a div').length > 2) {
  4401. shuffleButton.classList.add('nextsongcontrolbutton', 'shuffle');
  4402. shuffleButton.setAttribute('title', 'Shuffle');
  4403. const shuffleButtonIcon = shuffleButton.appendChild(document.createElement('div'));
  4404. shuffleButtonIcon.classList.add('nextsongcontrolicon');
  4405. shuffleButtonIcon.style.backgroundPositionY = '0px';
  4406. shuffleButton.addEventListener('click', function () {
  4407. this.classList.toggle('active');
  4408. });
  4409. }
  4410. const findLastSongIndex = function () {
  4411. const allDiv = document.querySelectorAll('#track_table a div');
  4412. const nextDiv = document.querySelector('#track_table a div.playing');
  4413. if (!nextDiv) {
  4414. return allDiv.length - 1;
  4415. }
  4416. for (let i = 1; i < allDiv.length; i++) {
  4417. if (allDiv[i] === nextDiv) {
  4418. return i - 1;
  4419. }
  4420. }
  4421. return -1;
  4422. };
  4423. const albumPageAudioOnEnded = function (ev) {
  4424. const allDiv = document.querySelectorAll('#track_table a div');
  4425. if (repeatButton.dataset.repeat === 'one') {
  4426. // Click on last song again
  4427. if (allDiv.length > 0) {
  4428. allDiv[findLastSongIndex()].click();
  4429. } else {
  4430. // No tracklist, click on play button
  4431. document.querySelector('#trackInfoInner .inline_player .playbutton').click();
  4432. }
  4433. } else if (shuffleButton.classList.contains('active') && allDiv.length > 1) {
  4434. // Find last song
  4435. const lastSongIndex = findLastSongIndex();
  4436. // Set a random song (that is not the last song)
  4437. let index = lastSongIndex;
  4438. while (index === lastSongIndex) {
  4439. index = randomIndex(allDiv.length);
  4440. }
  4441. if (index !== lastSongIndex + 1) {
  4442. allDiv[index].click();
  4443. }
  4444. } else if (repeatButton.dataset.repeat === 'all') {
  4445. if (findLastSongIndex() === allDiv.length - 1) {
  4446. if (allDiv[0]) {
  4447. allDiv[0].click(); // Click on first song's play button
  4448. } else {
  4449. // No tracklist, click on play button
  4450. document.querySelector('#trackInfoInner .inline_player .playbutton').click();
  4451. }
  4452. }
  4453. }
  4454. };
  4455. let lastMediaHubTitle = null;
  4456. const onNotificationClick = function () {
  4457. if (!document.querySelector('#trackInfoInner .inline_player .nextbutton').classList.contains('hiddenelem')) {
  4458. document.querySelector('#trackInfoInner .inline_player .nextbutton').click();
  4459. }
  4460. };
  4461. const updateChromePositionState = function () {
  4462. if (audioAlbumPage && 'mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
  4463. navigator.mediaSession.setPositionState({
  4464. duration: audioAlbumPage.duration || 180,
  4465. playbackRate: audioAlbumPage.playbackRate,
  4466. position: audioAlbumPage.currentTime
  4467. });
  4468. }
  4469. };
  4470. const albumPageUpdateMediaHubListener = function albumPageUpdateMediaHub() {
  4471. const TralbumData = unsafeWindow.TralbumData;
  4472. const title = document.querySelector('#trackInfoInner .inline_player .title').textContent.trim();
  4473. if (lastMediaHubTitle === title) {
  4474. return;
  4475. }
  4476. lastMediaHubTitle = title;
  4477.  
  4478. // Notification
  4479. if (allFeatures.nextSongNotifications.enabled && 'notification' in GM) {
  4480. GM.notification({
  4481. title: document.location.host,
  4482. text: title + '\nby ' + TralbumData.artist + '\nfrom ' + TralbumData.current.title,
  4483. image: `https://f4.bcbits.com/img/a${TralbumData.current.art_id}_2.jpg`,
  4484. highlight: false,
  4485. silent: true,
  4486. timeout: NOTIFICATION_TIMEOUT,
  4487. onclick: onNotificationClick
  4488. });
  4489. }
  4490.  
  4491. // Media hub
  4492. if ('mediaSession' in navigator) {
  4493. if (audioAlbumPage) {
  4494. navigator.mediaSession.playbackState = !audioAlbumPage.paused ? 'playing' : 'paused';
  4495. updateChromePositionState();
  4496. }
  4497.  
  4498. // Pre load image to get dimension
  4499. const cover = document.createElement('img');
  4500. cover.onload = function onCoverLoaded() {
  4501. navigator.mediaSession.metadata = new MediaMetadata({
  4502. title,
  4503. artist: TralbumData.artist,
  4504. album: TralbumData.current.title,
  4505. artwork: [{
  4506. src: cover.src,
  4507. sizes: `${cover.width}x${cover.height}`,
  4508. type: 'image/jpeg'
  4509. }]
  4510. });
  4511. };
  4512. cover.src = `https://f4.bcbits.com/img/a${TralbumData.current.art_id}_2.jpg`;
  4513. if (!document.querySelector('#trackInfoInner .inline_player .prevbutton').classList.contains('hiddenelem')) {
  4514. navigator.mediaSession.setActionHandler('previoustrack', () => document.querySelector('#trackInfoInner .inline_player .prevbutton').click());
  4515. } else {
  4516. navigator.mediaSession.setActionHandler('previoustrack', null);
  4517. }
  4518. if (!document.querySelector('#trackInfoInner .inline_player .nextbutton').classList.contains('hiddenelem')) {
  4519. navigator.mediaSession.setActionHandler('nexttrack', () => document.querySelector('#trackInfoInner .inline_player .nextbutton').click());
  4520. } else {
  4521. navigator.mediaSession.setActionHandler('nexttrack', null);
  4522. }
  4523. if (audioAlbumPage) {
  4524. navigator.mediaSession.setActionHandler('play', function () {
  4525. audioAlbumPage.play();
  4526. navigator.mediaSession.playbackState = 'playing';
  4527. });
  4528. navigator.mediaSession.setActionHandler('pause', function () {
  4529. audioAlbumPage.pause();
  4530. navigator.mediaSession.playbackState = 'paused';
  4531. });
  4532. navigator.mediaSession.setActionHandler('seekbackward', function (event) {
  4533. const skipTime = event.seekOffset || DEFAULTSKIPTIME;
  4534. audioAlbumPage.currentTime = Math.max(audioAlbumPage.currentTime - skipTime, 0);
  4535. updateChromePositionState();
  4536. });
  4537. navigator.mediaSession.setActionHandler('seekforward', function (event) {
  4538. const skipTime = event.seekOffset || DEFAULTSKIPTIME;
  4539. audioAlbumPage.currentTime = Math.min(audioAlbumPage.currentTime + skipTime, audioAlbumPage.duration);
  4540. updateChromePositionState();
  4541. });
  4542. try {
  4543. navigator.mediaSession.setActionHandler('stop', function () {
  4544. audioAlbumPage.pause();
  4545. audioAlbumPage.currentTime = 0;
  4546. navigator.mediaSession.playbackState = 'paused';
  4547. });
  4548. } catch (error) {
  4549. console.warn('Warning! The "stop" media session action is not supported.');
  4550. }
  4551. try {
  4552. navigator.mediaSession.setActionHandler('seekto', function (event) {
  4553. if (event.fastSeek && 'fastSeek' in audioAlbumPage) {
  4554. audioAlbumPage.fastSeek(event.seekTime);
  4555. return;
  4556. }
  4557. audioAlbumPage.currentTime = event.seekTime;
  4558. updateChromePositionState();
  4559. });
  4560. } catch (error) {
  4561. console.warn('Warning! The "seekto" media session action is not supported.');
  4562. }
  4563. }
  4564. }
  4565. };
  4566. audioAlbumPage.addEventListener('ended', albumPageAudioOnEnded);
  4567. audioAlbumPage.addEventListener('play', albumPageUpdateMediaHubListener);
  4568. audioAlbumPage.addEventListener('ended', albumPageUpdateMediaHubListener);
  4569. }
  4570. function clickAddToWishlist() {
  4571. const wishButton = document.querySelector('#collect-item>*');
  4572. if (!wishButton) {
  4573. window.setTimeout(clickAddToWishlist, 300);
  4574. return;
  4575. }
  4576. wishButton.click();
  4577. if (document.querySelector('#collection-main a')) {
  4578. // if logged in, the click should be successful, so try to close the window
  4579. window.setTimeout(window.close, 1000);
  4580. }
  4581. }
  4582. async function addReleaseDateButton() {
  4583. await sleep(1000); // Wait a second for share-collect-controls to load (it is slow sometimes)
  4584. const TralbumData = unsafeWindow.TralbumData;
  4585. const now = new Date();
  4586. const releaseDate = new Date(TralbumData.current.release_date);
  4587. const days = parseInt(Math.ceil((releaseDate - now) / (1000 * 60 * 60 * 24)));
  4588. if (releaseDate < now) {
  4589. return; // Release date is in the past
  4590. }
  4591.  
  4592. const key = albumKey(TralbumData.url);
  4593. addStyle(`
  4594. .releaseReminderButton {
  4595. font-size:13px;
  4596. font-weight:700;
  4597. cursor:pointer;
  4598. transition: border 500ms, padding 500ms
  4599. }
  4600. .releaseReminderButton.active {
  4601. border-radius:5px;
  4602. padding:0px 5px;
  4603. border:#3fb32f66 solid 2px
  4604. }
  4605. .releaseReminderButton:hover .releaseLabel {
  4606. text-decoration:underline
  4607. }
  4608. `);
  4609. let parent = document.querySelector('.share-collect-controls');
  4610. if (!parent) {
  4611. parent = document.querySelector('.middleColumn');
  4612. }
  4613. if (!parent) {
  4614. // Try again in a second
  4615. return window.setTimeout(addReleaseDateButton, 1000);
  4616. }
  4617. const div = parent.appendChild(document.createElement('div'));
  4618. div.style = 'margin-top:4px';
  4619. const span = div.appendChild(document.createElement('span'));
  4620. span.className = 'custom-link-color releaseReminderButton';
  4621. span.title = 'Releases ' + dateFormaterRelease(releaseDate);
  4622. const daysStr = days === 1 ? 'tomorrow' : `in ${days} days`;
  4623. span.innerHTML = `<span>\u23F0</span> <span class="releaseLabel">Notify <time datetime="${releaseDate.toISOString()}">${daysStr}</time></span>`;
  4624. span.addEventListener('click', ev => toggleReleaseReminder(ev, span));
  4625. GM.getValue('releasereminder', '{}').then(function (str) {
  4626. const releaseReminderData = JSON.parse(str);
  4627. if (key in releaseReminderData) {
  4628. span.classList.add('active');
  4629. span.innerHTML = `<span>\u23F0</span> <span class="releaseLabel">Reminder set (<time datetime="${releaseDate.toISOString()}">${daysStr}</time>)</span>`;
  4630. }
  4631. });
  4632. }
  4633. async function toggleReleaseReminder(ev, span) {
  4634. const TralbumData = unsafeWindow.TralbumData;
  4635. const key = albumKey(TralbumData.url);
  4636. const releaseReminderData = JSON.parse(await GM.getValue('releasereminder', '{}'));
  4637. if (key in releaseReminderData) {
  4638. delete releaseReminderData[key];
  4639. } else {
  4640. releaseReminderData[key] = {
  4641. albumCover: `https://f4.bcbits.com/img/a${TralbumData.art_id}_2.jpg`,
  4642. releaseDate: TralbumData.current.release_date,
  4643. artist: TralbumData.artist,
  4644. title: TralbumData.current.title
  4645. };
  4646. }
  4647. await GM.setValue('releasereminder', JSON.stringify(releaseReminderData));
  4648. if (span) {
  4649. const releaseDate = new Date(TralbumData.current.release_date);
  4650. const now = new Date();
  4651. const days = parseInt(Math.ceil((releaseDate - now) / (1000 * 60 * 60 * 24)));
  4652. const daysStr = days === 1 ? 'tomorrow' : `in ${days} days`;
  4653. if (key in releaseReminderData) {
  4654. span.classList.add('active');
  4655. span.innerHTML = `<span>\u23F0</span> <span class="releaseLabel">Reminder set (<time datetime="${releaseDate.toISOString()}">${daysStr}</time>)</span>`;
  4656. } else {
  4657. span.classList.remove('active');
  4658. span.innerHTML = `<span>\u23F0</span> <span class="releaseLabel">Notify <time datetime="${releaseDate.toISOString()}">${daysStr}</time></span>`;
  4659. }
  4660. }
  4661. }
  4662. async function removeReleaseReminder(ev) {
  4663. ev.preventDefault();
  4664. const key = this.parentNode.dataset.key;
  4665. const releaseReminderData = JSON.parse(await GM.getValue('releasereminder', '{}'));
  4666. if (key in releaseReminderData) {
  4667. delete releaseReminderData[key];
  4668. await GM.setValue('releasereminder', JSON.stringify(releaseReminderData));
  4669. }
  4670. this.parentNode.remove();
  4671. }
  4672. function maximizePastReleases() {
  4673. document.getElementById('pastreleases').style.opacity = 0.0;
  4674. window.setTimeout(() => showPastReleases(null, true), 500);
  4675. document.getElementById('pastreleases').removeEventListener('click', maximizePastReleases);
  4676. }
  4677. async function showPastReleases(ev, forceShow) {
  4678. let hideDate = await GM.getValue('pastreleaseshidden', false);
  4679. const releaseReminderData = JSON.parse(await GM.getValue('releasereminder', '{}'));
  4680. const releases = [];
  4681. let pastReleasesCounter = 0;
  4682. const now = new Date();
  4683. now.setHours(23);
  4684. now.setMinutes(59);
  4685. for (const key in releaseReminderData) {
  4686. releaseReminderData[key].key = key;
  4687. releaseReminderData[key].date = new Date(releaseReminderData[key].releaseDate);
  4688. releaseReminderData[key].past = now >= releaseReminderData[key].date;
  4689. if (releaseReminderData[key].past) {
  4690. pastReleasesCounter++;
  4691. }
  4692. releases.push(releaseReminderData[key]);
  4693. }
  4694. releases.sort((a, b) => b.date - a.date);
  4695. if (releases.length === 0 || pastReleasesCounter === 0) {
  4696. return;
  4697. }
  4698. if (!document.getElementById('pastreleases')) {
  4699. addStyle(pastreleasesCSS);
  4700. }
  4701. const div = document.body.appendChild(document.getElementById('pastreleases') || document.createElement('div'));
  4702. div.setAttribute('id', 'pastreleases');
  4703. div.style.maxHeight = document.documentElement.clientHeight - 50 + 'px';
  4704. div.style.maxWidth = document.documentElement.clientWidth - 100 + 'px';
  4705. if (document.getElementById('discographyplayer') && !allFeatures.discographyplayerSidebar.enabled) {
  4706. div.style.bottom = document.getElementById('discographyplayer').clientHeight + 10 + 'px';
  4707. }
  4708. window.setTimeout(function () {
  4709. div.style.opacity = 1.0;
  4710. }, 200);
  4711. div.innerHTML = '';
  4712. const table = div.appendChild(document.createElement('div'));
  4713. table.classList.add('tablediv');
  4714. const firstRow = table.appendChild(document.createElement('div'));
  4715. firstRow.classList.add('header');
  4716. firstRow.appendChild(document.createTextNode('\u23F0'));
  4717. firstRow.appendChild(document.createElement('span'));
  4718. if (!forceShow && hideDate && !isNaN(hideDate = new Date(hideDate)) && new Date() - hideDate < 1000 * 60 * 60) {
  4719. firstRow.appendChild(document.createTextNode(`${pastReleasesCounter} release` + (pastReleasesCounter === 1 ? '' : 's')));
  4720. table.addEventListener('click', maximizePastReleases);
  4721. return;
  4722. } else {
  4723. GM.setValue('pastreleaseshidden', '');
  4724. }
  4725. const upcoming = firstRow.appendChild(document.createElement('span'));
  4726. if (releases.length !== pastReleasesCounter) {
  4727. upcoming.appendChild(document.createTextNode(' Show upcoming'));
  4728. upcoming.classList.add('upcoming');
  4729. upcoming.addEventListener('click', function () {
  4730. document.querySelectorAll('#pastreleases .future').forEach(function (el) {
  4731. el.style.display = 'table-row';
  4732. });
  4733. this.remove();
  4734. });
  4735. }
  4736. const controls = firstRow.appendChild(document.createElement('span'));
  4737. controls.classList.add('controls');
  4738. const refresh = controls.appendChild(document.createElement('span'));
  4739. refresh.setAttribute('title', 'Update');
  4740. refresh.addEventListener('click', function () {
  4741. document.getElementById('pastreleases').style.opacity = 0.0;
  4742. window.setTimeout(() => showPastReleases(null, true), 1200);
  4743. });
  4744. refresh.appendChild(document.createTextNode(NOEMOJI ? 'Refresh' : '⟳'));
  4745. const close = controls.appendChild(document.createElement('span'));
  4746. close.setAttribute('title', 'Hide');
  4747. close.addEventListener('click', function () {
  4748. GM.setValue('pastreleaseshidden', new Date().toJSON());
  4749. document.getElementById('pastreleases').style.opacity = 0.0;
  4750. window.setTimeout(function () {
  4751. document.getElementById('pastreleases').remove();
  4752. }, 700);
  4753. });
  4754. close.appendChild(document.createTextNode('X'));
  4755. releases.forEach(function (release) {
  4756. const days = parseInt(Math.ceil((release.date - now) / (1000 * 60 * 60 * 24)));
  4757. const daysStr = days === 1 ? 'tomorrow' : `in ${days} days`;
  4758. let title = `${release.artist} - ${release.title}`;
  4759. const entry = table.appendChild(document.createElement('a'));
  4760. entry.setAttribute('title', title);
  4761. entry.dataset.key = release.key;
  4762. entry.classList.add('entry');
  4763. entry.classList.add(release.past ? 'past' : 'future');
  4764. entry.setAttribute('href', document.location.protocol + '//' + release.key);
  4765. entry.setAttribute('target', '_blank');
  4766. const removeButton = entry.appendChild(document.createElement('span'));
  4767. removeButton.setAttribute('title', 'Remove album');
  4768. removeButton.classList.add('remove');
  4769. removeButton.appendChild(document.createTextNode(NOEMOJI ? 'X' : '╳'));
  4770. removeButton.addEventListener('click', removeReleaseReminder);
  4771. const time = entry.appendChild(document.createElement('time'));
  4772. time.setAttribute('datetime', release.date.toISOString());
  4773. time.setAttribute('title', 'Releases ' + dateFormaterRelease(release.date));
  4774. if (release.past) {
  4775. time.appendChild(document.createTextNode(dateFormaterNumeric(release.date)));
  4776. } else {
  4777. time.appendChild(document.createTextNode(daysStr));
  4778. }
  4779. const span = entry.appendChild(document.createElement('span'));
  4780. span.classList.add('title');
  4781. title = title.length < 60 ? title : title.substr(0, 57) + '…';
  4782. span.appendChild(document.createTextNode(' ' + title));
  4783. const image = entry.appendChild(document.createElement('div'));
  4784. image.classList.add('image');
  4785. image.style.backgroundRepeat = 'no-repeat';
  4786. image.style.backgroundSize = 'contain';
  4787. image.style.backgroundImage = `url(${release.albumCover})`;
  4788. });
  4789. }
  4790. function showTagSearchForm() {
  4791. const menuA = document.querySelector('#bcsde_tagsearchbutton');
  4792. menuA.style.display = 'none';
  4793. if (!document.getElementById('bcsde_tagsearchform')) {
  4794. addStyle(`
  4795. #bcsde_tagsearchform {
  4796. margin:0px 7px;
  4797. }
  4798. #bcsde_tagsearchform_tags {
  4799. display: inline-block;
  4800. list-style: none;
  4801. padding: 0;
  4802. }
  4803. #bcsde_tagsearchform_tags li {
  4804. display:inline;
  4805. background:#f2eaea8a;
  4806. border: 1px solid rgb(225, 45, 5);
  4807. border-radius: 15px;
  4808. padding: 2px 10px 2px 2px;
  4809. font-size: 13px;
  4810. font-weight: 500;
  4811. }
  4812. #bcsde_tagsearchform_tags li svg {
  4813. filter: invert(100%);
  4814. fill:rgb(225, 45, 5);
  4815. vertical-align: middle;
  4816. }
  4817. #bcsde_tagsearchform_tags li .checkmark-icon {
  4818. display:inline-block;
  4819. }
  4820. #bcsde_tagsearchform_tags li .close-icon {
  4821. display:none;
  4822. }
  4823. #bcsde_tagsearchform_tags li:hover .checkmark-icon {
  4824. display:none;
  4825. }
  4826. #bcsde_tagsearchform_tags li:hover .close-icon {
  4827. display:inline-block;
  4828. }
  4829. #bcsde_tagsearchform button {
  4830. margin: 3px;
  4831. color: black !important;
  4832. }
  4833. #bcsde_tagsearchform_input {
  4834. background-color: #DFDFDF;
  4835. padding: 10px 30px 10px 10px;
  4836. font-size: 14px;
  4837. border: none;
  4838. width: 150px;
  4839. color: #333;
  4840. margin: 6px 0;
  4841. border-radius: 3px;
  4842. box-sizing: border-box;
  4843. input-select:auto;
  4844. -webkit-user-select:auto;
  4845. }
  4846. #bcsde_tagsearchform_suggestions {
  4847. list-style: none;
  4848. margin: 0;
  4849. position: absolute;
  4850. z-index: 10;
  4851. background: #FFF;
  4852. visibility: hidden;
  4853. border: 1px solid #000;
  4854. font-weight: normal;
  4855. padding: 8px 0;
  4856. opacity:0;
  4857. transition:visibility 200ms linear,opacity 200ms linear;
  4858. ${darkModeModeCurrent === true ? 'filter: invert(85%);' : ''}
  4859. }
  4860. #bcsde_tagsearchform_suggestions.visible {
  4861. visibility:visible;
  4862. opacity:1;
  4863. }
  4864. #bcsde_tagsearchform_suggestions li {
  4865. padding: 8px 10px;
  4866. cursor: pointer;
  4867. list-style: none;
  4868. margin: 0;
  4869. display: list-item;
  4870. text-align: left;
  4871. }
  4872. #bcsde_tagsearchform_suggestions li:hover,#bcsde_tagsearchform_suggestions li:focus {
  4873. background: #F3F3F3;
  4874. }
  4875. `);
  4876. const div = document.createElement('div');
  4877. div.setAttribute('id', 'bcsde_tagsearchform');
  4878. menuA.parentNode.appendChild(div);
  4879. const tagsHolder = div.appendChild(document.createElement('ul'));
  4880. tagsHolder.setAttribute('id', 'bcsde_tagsearchform_tags');
  4881. const m = document.location.href.match(/\/tag\/([A-Za-z0-9-]+)(\?tab=all_releases&t=(.+))?/); // https://bandcamp.com/tag/metal?tab=all_releases&t=post-punk%2Cdark
  4882. const tags = [];
  4883. if (m) {
  4884. tags.push(m[1]);
  4885. if (m[3]) {
  4886. tags.push(...m[3].split('&')[0].split('#')[0].split('%2C'));
  4887. }
  4888. }
  4889. tags.forEach(tag => {
  4890. tagsHolder.appendChild(tagSearchLabel(tag, tag.replace('-', ' ')));
  4891. });
  4892. const button = div.appendChild(document.createElement('button'));
  4893. button.appendChild(document.createTextNode('Go'));
  4894. button.addEventListener('click', openTagSearch);
  4895. const input = div.appendChild(document.createElement('input'));
  4896. input.setAttribute('type', 'text');
  4897. input.setAttribute('id', 'bcsde_tagsearchform_input');
  4898. input.setAttribute('placeholder', 'tag search');
  4899. input.addEventListener('keyup', tagSearchInputChange);
  4900. const suggestions = div.appendChild(document.createElement('ol'));
  4901. suggestions.setAttribute('id', 'bcsde_tagsearchform_suggestions');
  4902. if (document.querySelector('#corphome-autocomplete-form ul.hd-nav.corp-nav .log-in-link')) {
  4903. // Homepage and not logged in -> make some room by removing the other list items from the nav
  4904. document.querySelectorAll('#corphome-autocomplete-form ul.hd-nav.corp-nav>li:not([class~="menubar-item-tag-search"])').forEach(listItem => listItem.remove());
  4905. }
  4906. } else {
  4907. document.querySelector('#bcsde_tagsearchform').style.display = '';
  4908. }
  4909. }
  4910. function tagSearchLabel(tagNormName, tagName) {
  4911. const li = document.createElement('li');
  4912. li.dataset.tagNormName = tagNormName;
  4913. li.dataset.name = tagName;
  4914. const remove = li.appendChild(document.createElement('span'));
  4915. remove.addEventListener('click', function () {
  4916. this.parentNode.remove();
  4917. });
  4918. remove.innerHTML = `
  4919. <svg class="checkmark-icon" width="16" height="16" viewBox="0 0 24 24">
  4920. <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#material-done"></use>
  4921. </svg>
  4922. <svg class="close-icon" width="16" height="16" viewBox="0 0 24 24">
  4923. <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#material-close"></use>
  4924. </svg>
  4925. `;
  4926. li.appendChild(document.createTextNode(tagName));
  4927. return li;
  4928. }
  4929. let ivTagSearchInput = null;
  4930. function tagSearchInputChange(ev) {
  4931. clearInterval(ivTagSearchInput);
  4932. if (ev.key === 'Enter') {
  4933. const input = document.getElementById('bcsde_tagsearchform_input');
  4934. if (input.value) {
  4935. useTagSuggestion(null, input.value);
  4936. return;
  4937. }
  4938. }
  4939. ivTagSearchInput = window.setTimeout(showTagSuggestions, 300);
  4940. }
  4941. function showTagSuggestions() {
  4942. const input = document.getElementById('bcsde_tagsearchform_input');
  4943. const suggestions = document.getElementById('bcsde_tagsearchform_suggestions');
  4944. if (!input.value.trim()) {
  4945. suggestions.classList.remove('visible');
  4946. return;
  4947. }
  4948. getTagSuggestions(input.value).then(data => {
  4949. let found = false;
  4950. if (data.ok && 'matching_tags' in data) {
  4951. suggestions.innerHTML = '';
  4952. suggestions.classList.add('visible');
  4953. suggestions.style.left = input.offsetLeft + 'px';
  4954. data.matching_tags.forEach(result => {
  4955. found = true;
  4956. const li = suggestions.appendChild(document.createElement('li'));
  4957. li.dataset.tagNormName = result.tag_norm_name;
  4958. li.dataset.name = result.tag_name;
  4959. li.addEventListener('click', useTagSuggestion);
  4960. li.appendChild(document.createTextNode(result.tag_name));
  4961. });
  4962. }
  4963. if (!found) {
  4964. if (input.value.trim()) {
  4965. const li = suggestions.appendChild(document.createElement('li'));
  4966. li.dataset.tagNormName = input.value.replace(/\s+/, '-');
  4967. li.dataset.name = input.value;
  4968. li.addEventListener('click', useTagSuggestion);
  4969. li.appendChild(document.createTextNode(input.value));
  4970. } else {
  4971. suggestions.classList.remove('visible');
  4972. }
  4973. }
  4974. });
  4975. }
  4976. function useTagSuggestion(ev, str = null) {
  4977. const suggestions = document.getElementById('bcsde_tagsearchform_suggestions');
  4978. const tagsHolder = document.getElementById('bcsde_tagsearchform_tags');
  4979. const input = document.getElementById('bcsde_tagsearchform_input');
  4980. let tagNormName;
  4981. let name;
  4982. if (str) {
  4983. // Use str
  4984. tagNormName = str.replace(/\s+/, '-');
  4985. name = str;
  4986. } else {
  4987. // Use tag that was clicked
  4988. tagNormName = this.dataset.tagNormName;
  4989. name = this.dataset.name;
  4990. }
  4991. tagsHolder.appendChild(tagSearchLabel(tagNormName, name));
  4992. suggestions.classList.remove('visible');
  4993. input.value = '';
  4994. input.focus();
  4995. }
  4996. function getTagSuggestions(query) {
  4997. const url = 'https://bandcamp.com/api/fansignup/1/search_tag';
  4998. return new Promise(function getTagSuggestionsPromise(resolve, reject) {
  4999. GM.xmlHttpRequest({
  5000. method: 'POST',
  5001. data: JSON.stringify({
  5002. count: 20,
  5003. search_term: query
  5004. }),
  5005. url,
  5006. onload: function getTagSuggestionsOnLoad(response) {
  5007. if (!response.responseText || response.responseText.indexOf('400 Bad Request') !== -1) {
  5008. reject(new Error('Tag suggestions error: Too many cookies'));
  5009. return;
  5010. }
  5011. if (!response.responseText || response.responseText.indexOf('429 Too Many Requests') !== -1) {
  5012. reject(new Error('Tag suggestions error: 429 Too Many Requests'));
  5013. return;
  5014. }
  5015. let result = null;
  5016. try {
  5017. result = JSON.parse(response.responseText);
  5018. } catch (e) {
  5019. console.debug(response.responseText);
  5020. reject(e);
  5021. return;
  5022. }
  5023. resolve(result);
  5024. },
  5025. onerror: function getTagSuggestionsOnError(response) {
  5026. reject(new Error('error' in response ? response.error : 'getTagSuggestions failed with GM.xmlHttpRequest.onerror'));
  5027. }
  5028. });
  5029. });
  5030. }
  5031. function openTagSearch() {
  5032. // https://bandcamp.com/tag/metal?tab=all_releases&t=post-punk%2Cdark
  5033. this.innerHTML = 'Loading...';
  5034. const tagsHolder = document.getElementById('bcsde_tagsearchform_tags');
  5035. const tags = [...new Set(Array.from(tagsHolder.querySelectorAll('li')).map(li => li.dataset.tagNormName))];
  5036. if (!tags) {
  5037. return;
  5038. }
  5039. const url = `https://bandcamp.com/tag/${tags.shift()}?tab=all_releases&t=${tags.join('%2C')}`;
  5040. document.location.href = url;
  5041. }
  5042. function mainMenu(startBackup) {
  5043. addStyle(`
  5044. .deluxemenu {
  5045. position:fixed;
  5046. height:auto;
  5047. overflow:auto;
  5048. top:20px;
  5049. left:20px;
  5050. z-index:1102;
  5051. padding:5px;
  5052. transition: left 1s;
  5053. border:2px solid black;
  5054. border-radius:10px;
  5055. color:black;
  5056. background:white;
  5057. }
  5058. .deluxemenu input{
  5059. box-shadow: 2px 2px 5px #5555;
  5060. transition: box-shadow 500ms;
  5061. }
  5062. .deluxemenu fieldset{
  5063. border: 1px solid #000a;
  5064. border-radius: 4px;
  5065. box-shadow: 1px 1px 3px #0005;
  5066. }
  5067. .deluxemenu fieldset legend{
  5068. margin-left: 10px;
  5069. color: #000a
  5070. }
  5071. .breathe {
  5072. animation: breathe 1.5s linear infinite
  5073. }
  5074. @keyframes breathe {
  5075. 50% { opacity: 0.3 }
  5076. }
  5077. .errorblink {
  5078. animation: errorblink 1.5s linear infinite;
  5079. border: 2px solid red;
  5080. }
  5081. @keyframes errorblink {
  5082. 50% { border-color:#6a0c41 }
  5083. }
  5084. .deluxemenu ul {
  5085. margin: 0px;
  5086. padding: 0px 0px 0px 10px;
  5087. list-style:disc;
  5088. }
  5089. .deluxemenu ul li{
  5090. margin: 0px;
  5091. padding: 0px;
  5092. }
  5093. `);
  5094. if (startBackup === true) {
  5095. exportMenu();
  5096. return;
  5097. }
  5098. if (document.querySelector('.deluxemenu')) {
  5099. return;
  5100. }
  5101.  
  5102. // Blur background
  5103. if (document.getElementById('centerWrapper')) {
  5104. document.getElementById('centerWrapper').style.filter = 'blur(4px)';
  5105. }
  5106. const main = document.body.appendChild(document.createElement('div'));
  5107. main.className = 'deluxemenu';
  5108. main.innerHTML = `<h2>${SCRIPT_NAME}</h2>
  5109. Source code license: <a target="_blank" href="https://github.com/cvzi/Bandcamp-script-deluxe-edition/blob/master/LICENSE">MIT</a><br>
  5110. Support: <a target="_blank" href="https://github.com/cvzi/Bandcamp-script-deluxe-edition">github.com/cvzi/Bandcamp-script-deluxe-edition</a><br>
  5111. Dark theme based on: <a target="_blank" href="https://userstyles.org/styles/171538/bandcamp-in-dark">"Bandcamp In Dark"</a> by <a target="_blank" href="https://userstyles.org/users/563391">Simonus</a><br>
  5112. Dev &amp; build tools used: <a target="_blank" href="https://github.com/cvzi/Bandcamp-script-deluxe-edition/blob/master/package.json#L43-L71">package.json</a><br>
  5113. Emoji: <a target="_blank" href="https://github.com/hfg-gmuend/openmoji">OpenMoji</a><br>
  5114. Javascript libraries used:<br><ul>
  5115. <li><a target="_blank" href="https://json5.org/">JSON5 - JSON for Humans</a> (MIT license)</li>
  5116. <li><a target="_blank" href="https://github.com/facebook/react">React</a> (MIT license)</li>
  5117. <li><a target="_blank" href="https://github.com/cvzi/genius-lyrics-userscript/">GeniusLyrics.js</a> (GPLv3)</li>
  5118. </ul>
  5119. <h3>Options</h3>
  5120. `;
  5121. window.setTimeout(function moveMenuIntoView() {
  5122. main.style.maxHeight = document.documentElement.clientHeight - 150 + 'px';
  5123. main.style.maxWidth = document.documentElement.clientWidth - 40 + 'px';
  5124. main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth)) + 'px';
  5125. }, 0);
  5126. Promise.all([GM.getValue('volume', '0.7'), GM.getValue('myalbums', '{}'), GM.getValue('tralbumdata', '{}'), GM.getValue('enabledFeatures', false), GM.getValue('markasplayedThreshold', '10s')]).then(function allPromisesLoaded(values) {
  5127. // let volume = parseFloat(values[0])
  5128. // volume = Number.isNaN(volume) ? 0.7 : volume
  5129. const myalbums = JSON.parse(values[1]);
  5130. const tralbumdata = JSON.parse(values[2]);
  5131. getEnabledFeatures(values[3]);
  5132. const markasplayedThreshold = values[4];
  5133. const checkboxOnChange = async function onCheckboxChange() {
  5134. const input = this;
  5135. getEnabledFeatures(await GM.getValue('enabledFeatures', false));
  5136. allFeatures[input.name].enabled = input.checked;
  5137. await GM.setValue('enabledFeatures', JSON.stringify(allFeatures));
  5138. input.style.boxShadow = '2px 2px 5px #0a0f';
  5139. window.setTimeout(function resetBoxShadowTimeout() {
  5140. input.style.boxShadow = '';
  5141. }, 3000);
  5142. updateMoreVisibility();
  5143. };
  5144. const thresholdOnChange = async function onThresholdChange() {
  5145. const input = this;
  5146. let value = input.value.trim();
  5147. const m = value.match(/^(\d+)(s|%)$/);
  5148. if (m && parseInt(m[1]) >= 0 && (m[2] === 's' || parseInt(m[1]) <= 100)) {
  5149. value = m[1] + m[2];
  5150. } else if (value.match(/^\d+$/) && parseInt(value.split('\n')[0]) >= 0) {
  5151. value = value.split('\n')[0] + 's';
  5152. } else {
  5153. window.alert('Format does not match!\nChoose either a time in seconds e.g. 10s or a percentage e.g. 50%');
  5154. return;
  5155. }
  5156. await GM.setValue('markasplayedThreshold', value);
  5157. input.value = value;
  5158. input.style.boxShadow = '2px 2px 5px #0a0f';
  5159. window.setTimeout(function resetBoxShadowTimeout() {
  5160. input.style.boxShadow = '';
  5161. }, 3000);
  5162. };
  5163. const updateMoreVisibility = function () {
  5164. for (const feature in allFeatures) {
  5165. if (document.getElementById('feature_' + feature + '_more_on')) {
  5166. document.getElementById('feature_' + feature + '_more_on').style.display = allFeatures[feature].enabled ? 'block' : 'none';
  5167. }
  5168. if (document.getElementById('feature_' + feature + '_more_off')) {
  5169. document.getElementById('feature_' + feature + '_more_off').style.display = allFeatures[feature].enabled ? 'none' : 'block';
  5170. }
  5171. }
  5172. };
  5173. for (const feature in allFeatures) {
  5174. const div = main.appendChild(document.createElement('div'));
  5175. const checkbox = div.appendChild(document.createElement('input'));
  5176. checkbox.type = 'checkbox';
  5177. checkbox.id = 'feature_' + feature;
  5178. checkbox.name = feature;
  5179. checkbox.checked = allFeatures[feature].enabled;
  5180. const label = div.appendChild(document.createElement('label'));
  5181. label.setAttribute('for', 'feature_' + feature);
  5182. label.innerHTML = allFeatures[feature].name;
  5183. checkbox.addEventListener('change', checkboxOnChange);
  5184. if (feature === 'markasplayedAuto') {
  5185. main.appendChild(document.createTextNode(' '));
  5186. const inputThreshold = div.appendChild(document.createElement('input'));
  5187. inputThreshold.type = 'text';
  5188. inputThreshold.value = markasplayedThreshold;
  5189. inputThreshold.size = 3;
  5190. inputThreshold.title = 'For example: 10s or 50%';
  5191. inputThreshold.id = 'feature_' + feature + '_threshold';
  5192. div.appendChild(document.createTextNode(' '));
  5193. const label = div.appendChild(document.createElement('label'));
  5194. label.setAttribute('for', 'feature_' + feature + '_threshold');
  5195. label.innerHTML = 'seconds or percentage.';
  5196. inputThreshold.addEventListener('change', thresholdOnChange);
  5197. }
  5198. if (feature in moreSettings) {
  5199. if (typeof moreSettings[feature] === 'function') {
  5200. const moreSettinsContainer = main.appendChild(document.createElement('fieldset'));
  5201. moreSettings[feature](moreSettinsContainer).then(function (v) {
  5202. if (v) {
  5203. moreSettinsContainer.appendChild(document.createElement('legend')).appendChild(document.createTextNode(v));
  5204. }
  5205. });
  5206. } else {
  5207. if ('true' in moreSettings[feature]) {
  5208. const moreSettinsContainerOn = main.appendChild(document.createElement('fieldset'));
  5209. moreSettinsContainerOn.setAttribute('id', 'feature_' + feature + '_more_on');
  5210. moreSettinsContainerOn.style.display = allFeatures[feature].enabled ? 'block' : 'none';
  5211. moreSettings[feature].true(moreSettinsContainerOn).then(function (v) {
  5212. if (v) {
  5213. moreSettinsContainerOn.appendChild(document.createElement('legend')).appendChild(document.createTextNode(v));
  5214. }
  5215. });
  5216. }
  5217. if ('false' in moreSettings[feature]) {
  5218. const moreSettinsContainerOff = main.appendChild(document.createElement('fieldset'));
  5219. moreSettinsContainerOff.setAttribute('id', 'feature_' + feature + '_more_off');
  5220. moreSettinsContainerOff.style.display = allFeatures[feature].enabled ? 'none' : 'block';
  5221. moreSettings[feature].false(moreSettinsContainerOff).then(function (v) {
  5222. if (v) {
  5223. moreSettinsContainerOff.appendChild(document.createElement('legend')).appendChild(document.createTextNode(v));
  5224. }
  5225. });
  5226. }
  5227. }
  5228. }
  5229. }
  5230.  
  5231. // Hint
  5232. main.appendChild(document.createElement('br'));
  5233. const p = main.appendChild(document.createElement('p'));
  5234. p.appendChild(document.createTextNode('Changes may require a page reload (F5)'));
  5235.  
  5236. // Bottom buttons
  5237. main.appendChild(document.createElement('br'));
  5238. const buttons = main.appendChild(document.createElement('div'));
  5239. const closeButton = buttons.appendChild(document.createElement('button'));
  5240. closeButton.appendChild(document.createTextNode('Close'));
  5241. closeButton.style.color = 'black';
  5242. closeButton.addEventListener('click', function onCloseButtonClick() {
  5243. document.querySelector('.deluxemenu').remove();
  5244. // Un-blur background
  5245. if (document.getElementById('centerWrapper')) {
  5246. document.getElementById('centerWrapper').style.filter = '';
  5247. }
  5248. });
  5249. const clearCacheButton = buttons.appendChild(document.createElement('button'));
  5250. clearCacheButton.appendChild(document.createTextNode('Clear cache'));
  5251. clearCacheButton.style.color = 'black';
  5252. clearCacheButton.addEventListener('click', function onClearCacheButtonClick() {
  5253. Promise.all([GM.setValue('genius_selectioncache', '{}'), GM.setValue('genius_requestcache', '{}'), GM.setValue('tralbumdata', '{}')]).then(function showClearedLabel() {
  5254. clearCacheButton.innerHTML = 'Cleared';
  5255. });
  5256. });
  5257. Promise.all([GM.getValue('genius_selectioncache', '{}'), GM.getValue('genius_requestcache', '{}')]).then(function (values) {
  5258. JSON.stringify(tralbumdata);
  5259. const bytesN = values[0].length - 2 + values[1].length - 2 + JSON.stringify(tralbumdata).length - 2;
  5260. const bytes = metricPrefix(bytesN, 1, 1024) + 'Bytes';
  5261. clearCacheButton.replaceChild(document.createTextNode('Clear cache (' + bytes + ')'), clearCacheButton.firstChild);
  5262. });
  5263. let myalbumsLength = 0;
  5264. for (const key in myalbums) {
  5265. if (myalbums[key].listened) {
  5266. myalbumsLength++;
  5267. }
  5268. }
  5269. const exportButton = buttons.appendChild(document.createElement('button'));
  5270. exportButton.appendChild(document.createTextNode('Export played albums (' + myalbumsLength + ')'));
  5271. exportButton.style.color = 'black';
  5272. exportButton.addEventListener('click', function onExportButtonClick() {
  5273. document.querySelector('.deluxemenu').remove();
  5274. exportMenu();
  5275. });
  5276. main.appendChild(document.createElement('br'));
  5277. main.appendChild(document.createElement('br'));
  5278. const donateLink = main.appendChild(document.createElement('a'));
  5279. const donateButton = donateLink.appendChild(document.createElement('button'));
  5280. donateButton.appendChild(document.createTextNode('\u2764\uFE0F Donate & Support'));
  5281. donateButton.style.color = '#e81224';
  5282. donateLink.setAttribute('href', 'https://cvzi.github.io/.github/');
  5283. donateLink.setAttribute('target', '_blank');
  5284. main.appendChild(document.createElement('br'));
  5285. main.appendChild(document.createElement('br'));
  5286. const developerButton = main.appendChild(document.createElement('button'));
  5287. developerButton.appendChild(document.createTextNode('Developer options'));
  5288. developerButton.style.color = 'black';
  5289. developerButton.addEventListener('click', function onDeveloperButtonClick() {
  5290. document.querySelector('.deluxemenu').remove();
  5291. developerMenu();
  5292. });
  5293. });
  5294. window.setTimeout(function moveMenuIntoView() {
  5295. let moveLeft = 0;
  5296. main.style.maxHeight = document.documentElement.clientHeight - 40 + 'px';
  5297. main.style.maxWidth = document.documentElement.clientWidth - 40 + 'px';
  5298. if (document.querySelector('#discographyplayer')) {
  5299. if (document.querySelector('#discographyplayer').clientHeight < 100) {
  5300. main.style.maxHeight = document.documentElement.clientHeight - 150 + 'px';
  5301. main.style.maxWidth = document.documentElement.clientWidth - 40 + 'px';
  5302. } else if (document.querySelector('#discographyplayer').clientHeight > 300) {
  5303. main.style.maxHeight = document.documentElement.clientHeight - 40 + 'px';
  5304. main.style.maxWidth = document.documentElement.clientWidth - 40 - document.querySelector('#discographyplayer').clientWidth + 'px';
  5305. moveLeft = document.querySelector('#discographyplayer').clientWidth + 20;
  5306. }
  5307. }
  5308. window.setTimeout(function () {
  5309. main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth) - moveLeft) + 'px';
  5310. }, 10);
  5311. }, 10);
  5312. }
  5313. function developerMenu() {
  5314. // Blur background
  5315. if (document.getElementById('centerWrapper')) {
  5316. document.getElementById('centerWrapper').style.filter = 'blur(4px)';
  5317. }
  5318. const main = document.body.appendChild(document.createElement('div'));
  5319. main.className = 'deluxedeveloper deluxemenu';
  5320. window.setTimeout(function moveMenuIntoView() {
  5321. main.style.maxHeight = document.documentElement.clientHeight - 40 + 'px';
  5322. main.style.maxWidth = document.documentElement.clientWidth - 40 + 'px';
  5323. main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth)) + 'px';
  5324. }, 0);
  5325. const h2 = main.appendChild(document.createElement('h2'));
  5326. h2.appendChild(document.createTextNode('Developer options'));
  5327. const table = main.appendChild(document.createElement('table'));
  5328.  
  5329. // Bottom buttons
  5330. main.appendChild(document.createElement('br'));
  5331. main.appendChild(document.createElement('br'));
  5332. const buttons = main.appendChild(document.createElement('div'));
  5333. const closeButton = buttons.appendChild(document.createElement('button'));
  5334. closeButton.appendChild(document.createTextNode('Close'));
  5335. closeButton.style.color = 'black';
  5336. closeButton.addEventListener('click', function onCloseButtonClick() {
  5337. document.querySelector('.deluxedeveloper').remove();
  5338. // Un-blur background
  5339. if (document.getElementById('centerWrapper')) {
  5340. document.getElementById('centerWrapper').style.filter = '';
  5341. }
  5342. });
  5343. let tr;
  5344. let td;
  5345. let input;
  5346. GM.getValue('myalbums', '{}').then(function myalbumsLoaded(myalbumsStr) {
  5347. const myalbums = JSON.parse(myalbumsStr);
  5348. const listenedAlbums = [];
  5349. for (const key in myalbums) {
  5350. if (myalbums[key].listened) {
  5351. listenedAlbums.push(myalbums[key]);
  5352. }
  5353. }
  5354. tr = table.appendChild(document.createElement('tr'));
  5355. td = tr.appendChild(document.createElement('td'));
  5356. td.appendChild(document.createTextNode('"myalbums" listened records'));
  5357. td = tr.appendChild(document.createElement('td'));
  5358. input = td.appendChild(document.createElement('input'));
  5359. input.type = 'text';
  5360. input.value = listenedAlbums.length.toString();
  5361. input.readOnly = true;
  5362. input.style.width = '200px';
  5363. tr = table.appendChild(document.createElement('tr'));
  5364. td = tr.appendChild(document.createElement('td'));
  5365. td.appendChild(document.createTextNode('"myalbums" string length'));
  5366. td = tr.appendChild(document.createElement('td'));
  5367. input = td.appendChild(document.createElement('input'));
  5368. input.type = 'text';
  5369. input.value = myalbumsStr.length.toString();
  5370. input.readOnly = true;
  5371. input.style.width = '200px';
  5372. tr = table.appendChild(document.createElement('tr'));
  5373. td = tr.appendChild(document.createElement('td'));
  5374. td.appendChild(document.createTextNode('"myalbums" size'));
  5375. td = tr.appendChild(document.createElement('td'));
  5376. input = td.appendChild(document.createElement('input'));
  5377. input.type = 'text';
  5378. input.value = humanBytes(new Blob([myalbumsStr]).size);
  5379. input.readOnly = true;
  5380. input.style.width = '200px';
  5381. });
  5382. GM.getValue('tralbumdata', '{}').then(function tralbumdataLoaded(tralbumdataStr) {
  5383. const tralbumdata = JSON.parse(tralbumdataStr);
  5384. tr = table.appendChild(document.createElement('tr'));
  5385. td = tr.appendChild(document.createElement('td'));
  5386. td.appendChild(document.createTextNode('"tralbumdataStr" entries'));
  5387. td = tr.appendChild(document.createElement('td'));
  5388. input = td.appendChild(document.createElement('input'));
  5389. input.type = 'text';
  5390. input.value = Object.keys(tralbumdata).length.toString();
  5391. input.readOnly = true;
  5392. input.style.width = '200px';
  5393. tr = table.appendChild(document.createElement('tr'));
  5394. td = tr.appendChild(document.createElement('td'));
  5395. td.appendChild(document.createTextNode('"tralbumdataStr" string length'));
  5396. td = tr.appendChild(document.createElement('td'));
  5397. input = td.appendChild(document.createElement('input'));
  5398. input.type = 'text';
  5399. input.value = tralbumdataStr.length.toString();
  5400. input.readOnly = true;
  5401. input.style.width = '200px';
  5402. tr = table.appendChild(document.createElement('tr'));
  5403. td = tr.appendChild(document.createElement('td'));
  5404. td.appendChild(document.createTextNode('"tralbumdataStr" size'));
  5405. td = tr.appendChild(document.createElement('td'));
  5406. input = td.appendChild(document.createElement('input'));
  5407. input.type = 'text';
  5408. input.value = humanBytes(new Blob([tralbumdataStr]).size);
  5409. input.readOnly = true;
  5410. input.style.width = '200px';
  5411. });
  5412. try {
  5413. GM.getValue('tralbumlibrary', '{}').then(function tralbumlibraryLoaded(tralbumlibraryStr) {
  5414. const tralbumlibrary = JSON.parse(tralbumlibraryStr);
  5415. tr = table.appendChild(document.createElement('tr'));
  5416. td = tr.appendChild(document.createElement('td'));
  5417. td.appendChild(document.createTextNode('"tralbumlibraryStr" entries'));
  5418. td = tr.appendChild(document.createElement('td'));
  5419. input = td.appendChild(document.createElement('input'));
  5420. input.type = 'text';
  5421. input.value = Object.keys(tralbumlibrary).length.toString();
  5422. input.readOnly = true;
  5423. input.style.width = '200px';
  5424. tr = table.appendChild(document.createElement('tr'));
  5425. td = tr.appendChild(document.createElement('td'));
  5426. td.appendChild(document.createTextNode('"tralbumlibraryStr" string length'));
  5427. td = tr.appendChild(document.createElement('td'));
  5428. input = td.appendChild(document.createElement('input'));
  5429. input.type = 'text';
  5430. input.value = tralbumlibraryStr.length.toString();
  5431. input.readOnly = true;
  5432. input.style.width = '200px';
  5433. tr = table.appendChild(document.createElement('tr'));
  5434. td = tr.appendChild(document.createElement('td'));
  5435. td.appendChild(document.createTextNode('"tralbumlibraryStr" size'));
  5436. td = tr.appendChild(document.createElement('td'));
  5437. input = td.appendChild(document.createElement('input'));
  5438. input.type = 'text';
  5439. input.value = humanBytes(new Blob([tralbumlibraryStr]).size);
  5440. input.readOnly = true;
  5441. input.style.width = '200px';
  5442. });
  5443. } catch (e) {
  5444. tr = table.appendChild(document.createElement('tr'));
  5445. td = tr.appendChild(document.createElement('td'));
  5446. td.appendChild(document.createTextNode('"tralbumlibraryStr"'));
  5447. td = tr.appendChild(document.createElement('td'));
  5448. td.appendChild(document.createTextNode('Error: ' + e.toString()));
  5449. }
  5450. window.setTimeout(function moveMenuIntoView() {
  5451. main.style.maxHeight = document.documentElement.clientHeight - 40 + 'px';
  5452. main.style.maxWidth = document.documentElement.clientWidth - 40 + 'px';
  5453. main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth)) + 'px';
  5454. }, 500);
  5455. }
  5456. function exportMenu(showClearButton) {
  5457. addStyle(`
  5458. .deluxeexportmenu table {
  5459. }
  5460.  
  5461. .deluxeexportmenu table tr>td {
  5462. color:black
  5463. }
  5464. .deluxeexportmenu table tr>td:nth-child(3) {
  5465. color:silver
  5466. }
  5467. .deluxeexportmenu textarea.animated{
  5468. box-shadow: 2px 2px 5px #5555;
  5469. transition: box-shadow 500ms;
  5470. }
  5471. .deluxeexportmenu .drophint {
  5472. position:absolute;
  5473. top:10%;
  5474. left:30%;
  5475. color:#0097ff;
  5476. font-size:3em;
  5477. display:none;
  5478. }
  5479. `);
  5480.  
  5481. // Blur background
  5482. if (document.getElementById('centerWrapper')) {
  5483. document.getElementById('centerWrapper').style.filter = 'blur(4px)';
  5484. }
  5485. const main = document.body.appendChild(document.createElement('div'));
  5486. main.className = 'deluxeexportmenu deluxemenu';
  5487. main.innerHTML = exportMenuHTML;
  5488. const drophint = main.querySelector('.drophint');
  5489. window.setTimeout(function moveMenuIntoView() {
  5490. main.style.maxHeight = document.documentElement.clientHeight - 40 + 'px';
  5491. main.style.maxWidth = document.documentElement.clientWidth - 40 + 'px';
  5492. main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth)) + 'px';
  5493. }, 0);
  5494. GM.getValue('myalbums', '{}').then(function myalbumsLoaded(myalbumsStr) {
  5495. const myalbums = JSON.parse(myalbumsStr);
  5496. const listenedAlbums = [];
  5497. for (const key in myalbums) {
  5498. if (myalbums[key].listened) {
  5499. listenedAlbums.push(myalbums[key]);
  5500. }
  5501. }
  5502. main.querySelector('h2').appendChild(document.createTextNode(' (' + listenedAlbums.length + ' records)'));
  5503. let format = '%artist% - %title%';
  5504. const formatAlbum = function formatAlbumStr(format, myAlbum) {
  5505. const releaseDate = new Date(myAlbum.releaseDate);
  5506. const listenedDate = new Date(myAlbum.listened);
  5507. const fields = {
  5508. '%artist%': () => myAlbum.artist,
  5509. '%title%': () => myAlbum.title,
  5510. '%cover%': () => myAlbum.albumCover,
  5511. '%url%': () => myAlbum.url,
  5512. '%releaseDate%': () => releaseDate.toISOString(),
  5513. '%listenedDate%': () => listenedDate.toISOString(),
  5514. '%releaseUnix%': () => parseInt(releaseDate.getTime() / 1000),
  5515. '%listenedUnix%': () => parseInt(listenedDate.getTime() / 1000),
  5516. '%releaseTimestamp%': () => releaseDate.getTime(),
  5517. '%listenedTimestamp%': () => listenedDate.getTime(),
  5518. '%releaseY%': () => releaseDate.getFullYear().toString().substring(2),
  5519. '%releaseYYYY%': () => releaseDate.getFullYear(),
  5520. '%releaseM%': () => releaseDate.getMonth() + 1,
  5521. '%releaseMM%': () => padd(releaseDate.getMonth() + 1, 2, '0'),
  5522. '%releaseMon%': () => releaseDate.toLocaleString(undefined, {
  5523. month: 'short'
  5524. }),
  5525. '%releaseMonth%': () => releaseDate.toLocaleString(undefined, {
  5526. month: 'long'
  5527. }),
  5528. '%releaseD%': () => releaseDate.getDate(),
  5529. '%releaseDD%': () => padd(releaseDate.getDate(), 2, '0'),
  5530. '%releaseDay%': () => releaseDate.toLocaleString(undefined, {
  5531. weekday: 'long'
  5532. }),
  5533. '%listenedY%': () => listenedDate.getFullYear().toString().substring(2),
  5534. '%listenedYYYY%': () => listenedDate.getFullYear(),
  5535. '%listenedM%': () => listenedDate.getMonth() + 1,
  5536. '%listenedMM%': () => padd(listenedDate.getMonth() + 1, 2, '0'),
  5537. '%listenedMon%': () => listenedDate.toLocaleString(undefined, {
  5538. month: 'short'
  5539. }),
  5540. '%listenedMonth%': () => listenedDate.toLocaleString(undefined, {
  5541. month: 'long'
  5542. }),
  5543. '%listenedD%': () => listenedDate.getDate(),
  5544. '%listenedDD%': () => padd(listenedDate.getDate(), 2, '0'),
  5545. '%listenedDay%': () => listenedDate.toLocaleString(undefined, {
  5546. weekday: 'long'
  5547. }),
  5548. '%json%': () => JSON.stringify(myAlbum),
  5549. '%json5%': () => JSON5.stringify(myAlbum)
  5550. };
  5551. for (const field in fields) {
  5552. if (format.includes(field)) {
  5553. try {
  5554. format = format.replace(field, fields[field]());
  5555. } catch (e) {
  5556. console.error('Could not format replace "' + field + '": ' + e);
  5557. }
  5558. }
  5559. }
  5560. return format;
  5561. };
  5562. const sortBy = function sortByCmp(sortKey) {
  5563. const cmps = {
  5564. playedAsc: function playedAsc(a, b) {
  5565. return -cmps.playedDesc(a, b);
  5566. },
  5567. playedDesc: function playedDesc(a, b) {
  5568. try {
  5569. return new Date(b.listened) - new Date(a.listened);
  5570. } catch (e) {
  5571. return 0;
  5572. }
  5573. },
  5574. releasedAsc: function releasedAsc(a, b) {
  5575. return -cmps.releasedDesc(a, b);
  5576. },
  5577. releasedDesc: function releasedDesc(a, b) {
  5578. try {
  5579. return new Date(b.releaseDate) - new Date(a.releaseDate);
  5580. } catch (e) {
  5581. return 0;
  5582. }
  5583. },
  5584. artist: function artist(a, b, fallbackToTitle) {
  5585. const d = a.artist.localeCompare(b.artist);
  5586. if (d === 0 && fallbackToTitle) {
  5587. return cmps.title(a, b, false);
  5588. } else {
  5589. return d;
  5590. }
  5591. },
  5592. title: function title(a, b, fallbackToArtist) {
  5593. const d = a.title.localeCompare(b.title);
  5594. if (d === 0 && fallbackToArtist) {
  5595. return cmps.artist(a, b, false);
  5596. } else {
  5597. return d;
  5598. }
  5599. }
  5600. };
  5601. listenedAlbums.sort(cmps[sortKey]);
  5602. };
  5603. const generate = function generateStr() {
  5604. const textarea = document.getElementById('export_output');
  5605. window.setTimeout(function generateStrAnimation() {
  5606. textarea.classList.remove('animated');
  5607. textarea.style.boxShadow = '2px 2px 5px #00af';
  5608. }, 0);
  5609. let str;
  5610. if (format === '%backup%') {
  5611. str = myalbumsStr;
  5612. } else {
  5613. const sortSelect = document.getElementById('sort_select');
  5614. sortBy(sortSelect.options[sortSelect.selectedIndex].value);
  5615. str = [];
  5616. for (let i = 0; i < listenedAlbums.length; i++) {
  5617. str.push(formatAlbum(format, listenedAlbums[i]));
  5618. }
  5619. str = str.join(navigator.platform.startsWith('Win') ? '\r\n' : '\n');
  5620. }
  5621. window.setTimeout(function generateStrAnimationSuccess() {
  5622. textarea.value = str;
  5623. textarea.classList.add('animated');
  5624. textarea.style.boxShadow = '2px 2px 5px #0a0f';
  5625. }, 50);
  5626. window.setTimeout(function generateStrResetAnimation() {
  5627. textarea.style.boxShadow = '';
  5628. }, 3000);
  5629. return str;
  5630. };
  5631. const inputFormatOnChange = async function onInputFormatChange() {
  5632. const input = this;
  5633. const formatExample = document.getElementById('format_example');
  5634. format = input.value;
  5635. formatExample.value = listenedAlbums.length > 0 ? formatAlbum(format, listenedAlbums[0]) : '';
  5636. formatExample.style.boxShadow = '2px 2px 5px #0a0f';
  5637. window.setTimeout(function resetBoxShadow() {
  5638. formatExample.style.boxShadow = '';
  5639. }, 3000);
  5640. };
  5641. const importData = function importDate(data) {
  5642. GM.getValue('myalbums', '{}').then(function myalbumsLoaded(myalbumsStr) {
  5643. let myalbums = JSON.parse(myalbumsStr);
  5644. myalbums = Object.assign(myalbums, data);
  5645. return GM.setValue('myalbums', JSON.stringify(myalbums));
  5646. }).then(function myalbumsSaved() {
  5647. document.getElementById('exportmenu_close').click();
  5648. window.setTimeout(() => exportMenu(true), 50);
  5649. });
  5650. };
  5651. const handleFiles = async function handleFilesAsync(fileList) {
  5652. if (fileList.length === 0) {
  5653. console.debug('fileList is empty');
  5654. return;
  5655. }
  5656. let data;
  5657. try {
  5658. data = await new Response(fileList[0]).json();
  5659. } catch (e) {
  5660. window.alert('Could not load file:\n' + e);
  5661. return;
  5662. }
  5663. const n = Object.keys(data).length;
  5664. if (window.confirm('Found ' + n + ' albums. Continue import and overwrite existing albums?')) {
  5665. importData(data);
  5666. }
  5667. };
  5668. const inputTable = main.appendChild(document.createElement('table'));
  5669. let tr;
  5670. let td;
  5671. tr = inputTable.appendChild(document.createElement('tr'));
  5672. td = tr.appendChild(document.createElement('td'));
  5673. const label = td.appendChild(document.createElement('label'));
  5674. label.setAttribute('for', 'export_format');
  5675. label.appendChild(document.createTextNode('Format:'));
  5676. td = tr.appendChild(document.createElement('td'));
  5677. const inputFormat = td.appendChild(document.createElement('input'));
  5678. inputFormat.type = 'text';
  5679. inputFormat.value = format;
  5680. inputFormat.id = 'export_format';
  5681. inputFormat.style.width = '600px';
  5682. inputFormat.addEventListener('change', inputFormatOnChange);
  5683. inputFormat.addEventListener('keyup', inputFormatOnChange);
  5684. tr = inputTable.appendChild(document.createElement('tr'));
  5685. td = tr.appendChild(document.createElement('td'));
  5686. td.appendChild(document.createTextNode('Example:'));
  5687. td = tr.appendChild(document.createElement('td'));
  5688. const inputExample = td.appendChild(document.createElement('input'));
  5689. inputExample.type = 'text';
  5690. inputExample.value = listenedAlbums.length > 0 ? formatAlbum(format, listenedAlbums[0]) : '';
  5691. inputExample.readOnly = true;
  5692. inputExample.id = 'format_example';
  5693. inputExample.style.width = '600px';
  5694. td = tr.appendChild(document.createElement('td'));
  5695. td.appendChild(document.createTextNode('Sort by:'));
  5696. td = tr.appendChild(document.createElement('td'));
  5697. const sortSelect = td.appendChild(document.createElement('select'));
  5698. sortSelect.id = 'sort_select';
  5699. sortSelect.innerHTML = `
  5700. <option value="playedDesc">Recent play first</option>
  5701. <option value="playedAsc">Recent play last</option>
  5702. <option value="releasedDesc">Recent release first</option>
  5703. <option value="releasedAsc">Recent release last</option>
  5704. <option value="artist">Artist A-Z</option>
  5705. <option value="title">Title A-Z</option>
  5706. `;
  5707. tr = inputTable.appendChild(document.createElement('tr'));
  5708. td = tr.appendChild(document.createElement('td'));
  5709. td.setAttribute('colspan', '2');
  5710. const generateButton = td.appendChild(document.createElement('button'));
  5711. generateButton.appendChild(document.createTextNode('Generate'));
  5712. generateButton.addEventListener('click', ev => generate());
  5713. const exportButton = td.appendChild(document.createElement('button'));
  5714. exportButton.appendChild(document.createTextNode('Export to file'));
  5715. exportButton.title = 'Download as a text file';
  5716. exportButton.addEventListener('click', function onExportFileButtonClick() {
  5717. const dateSuffix = new Date().toISOString().split('T')[0];
  5718. document.getElementById('export_download_link').download = 'bandcampPlayedAlbums_' + dateSuffix + '.txt';
  5719. document.getElementById('export_download_link').href = 'data:text/plain,' + encodeURIComponent(generate());
  5720. window.setTimeout(() => document.getElementById('export_download_link').click(), 50);
  5721. });
  5722. const backupButton = td.appendChild(document.createElement('button'));
  5723. backupButton.title = 'Backup to JSON file. Can be restored on another browser';
  5724. backupButton.appendChild(document.createTextNode('Backup'));
  5725. backupButton.addEventListener('click', function onBackupButtonClick() {
  5726. format = '%backup%';
  5727. document.getElementById('export_format').value = format;
  5728. document.getElementById('format_example').value = 'JSON dictionary';
  5729. const dateSuffix = new Date().toISOString().split('T')[0];
  5730. document.getElementById('export_download_link').download = 'bandcampPlayedAlbums_' + dateSuffix + '.json';
  5731. document.getElementById('export_download_link').href = 'data:application/json,' + encodeURIComponent(generate());
  5732. document.getElementById('export_clear_button').style.display = '';
  5733. GM.setValue('myalbums_lastbackup', Object.keys(myalbums).length + '#####' + new Date().toJSON());
  5734. window.setTimeout(() => document.getElementById('export_download_link').click(), 50);
  5735. });
  5736. const restoreButton = td.appendChild(document.createElement('button'));
  5737. restoreButton.title = 'Restore from JSON file backup';
  5738. restoreButton.appendChild(document.createTextNode('Restore'));
  5739. restoreButton.addEventListener('click', function onBackupButtonClick() {
  5740. inputFile.click();
  5741. });
  5742. const clearButton = td.appendChild(document.createElement('button'));
  5743. clearButton.appendChild(document.createTextNode('Clear played albums'));
  5744. clearButton.id = 'export_clear_button';
  5745. if (showClearButton !== true) {
  5746. clearButton.style.display = 'none';
  5747. }
  5748. clearButton.addEventListener('click', function onClearButtonClick() {
  5749. if (window.confirm('Remove all played albums?\n\nThis cannot be undone.')) {
  5750. if (window.confirm('Are you sure? Delete all played albums?')) {
  5751. GM.setValue('myalbums', '{}').then(function myalbumsSaved() {
  5752. document.getElementById('exportmenu_close').click();
  5753. window.setTimeout(exportMenu, 50);
  5754. });
  5755. }
  5756. }
  5757. });
  5758. const downloadA = td.appendChild(document.createElement('a'));
  5759. downloadA.id = 'export_download_link';
  5760. downloadA.href = '#';
  5761. downloadA.download = 'bandcamp_played_albums.txt';
  5762. downloadA.target = '_blank';
  5763. const inputFile = td.appendChild(document.createElement('input'));
  5764. inputFile.type = 'file';
  5765. inputFile.id = 'input_file';
  5766. inputFile.accept = '.txt,plain/text,.json,application/json';
  5767. inputFile.style.display = 'none';
  5768. inputFile.addEventListener('change', function onFileChanged(ev) {
  5769. handleFiles(this.files);
  5770. }, false);
  5771. main.addEventListener('dragenter', function dragenter(ev) {
  5772. ev.stopPropagation();
  5773. ev.preventDefault();
  5774. main.style.backgroundColor = '#c6daf9';
  5775. drophint.style.left = main.clientWidth / 2 - drophint.clientWidth / 2 + 'px';
  5776. drophint.style.display = 'block';
  5777. }, false);
  5778. main.addEventListener('dragleave', function dragleave(ev) {
  5779. main.style.backgroundColor = 'white';
  5780. drophint.style.display = 'none';
  5781. }, false);
  5782. main.addEventListener('dragover', function dragover(ev) {
  5783. ev.stopPropagation();
  5784. ev.preventDefault();
  5785. main.style.backgroundColor = '#c6daf9';
  5786. drophint.style.display = 'block';
  5787. }, false);
  5788. main.addEventListener('drop', function drop(ev) {
  5789. ev.stopPropagation();
  5790. ev.preventDefault();
  5791. main.style.backgroundColor = 'white';
  5792. drophint.style.display = 'none';
  5793. handleFiles(ev.dataTransfer.files);
  5794. }, false);
  5795. tr = inputTable.appendChild(document.createElement('tr'));
  5796. td = tr.appendChild(document.createElement('td'));
  5797. td.setAttribute('colspan', '3');
  5798. const textarea = td.appendChild(document.createElement('textarea'));
  5799. textarea.id = 'export_output';
  5800. textarea.style.width = Math.max(500, main.clientWidth - 50) + 'px';
  5801.  
  5802. // Bottom buttons
  5803. main.appendChild(document.createElement('br'));
  5804. main.appendChild(document.createElement('br'));
  5805. const buttons = main.appendChild(document.createElement('div'));
  5806. const closeButton = buttons.appendChild(document.createElement('button'));
  5807. closeButton.appendChild(document.createTextNode('Close'));
  5808. closeButton.id = 'exportmenu_close';
  5809. closeButton.style.color = 'black';
  5810. closeButton.addEventListener('click', function onCloseButtonClick() {
  5811. document.querySelector('.deluxeexportmenu').remove();
  5812. // Un-blur background
  5813. if (document.getElementById('centerWrapper')) {
  5814. document.getElementById('centerWrapper').style.filter = '';
  5815. }
  5816. });
  5817. });
  5818. window.setTimeout(function moveMenuIntoView() {
  5819. main.style.maxHeight = document.documentElement.clientHeight - 40 + 'px';
  5820. main.style.maxWidth = document.documentElement.clientWidth - 40 + 'px';
  5821. main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth)) + 'px';
  5822. }, 0);
  5823. }
  5824. function checkBackupStatus() {
  5825. GM.getValue('myalbums_lastbackup', '').then(function myalbumsLastBackupLoaded(value) {
  5826. if (!value || !value.includes('#####')) {
  5827. // Set current date (install date) as initial value
  5828. GM.setValue('myalbums_lastbackup', '0#####' + new Date().toJSON());
  5829. return;
  5830. }
  5831. const parts = value.split('#####');
  5832. const n0 = parseInt(parts[0]);
  5833. const lastBackup = new Date(parts[1]);
  5834. if (new Date() - lastBackup > BACKUP_REMINDER_DAYS * 86400000) {
  5835. GM.getValue('myalbums', '{}').then(function myalbumsLoaded(str) {
  5836. const n1 = Object.keys(JSON.parse(str)).length;
  5837. if (Math.abs(n0 - n1) > 10) {
  5838. showBackupHint(lastBackup, Math.abs(n0 - n1));
  5839. }
  5840. });
  5841. }
  5842. });
  5843. }
  5844. function showBackupHint(lastBackup, changedRecords) {
  5845. const since = timeSince(lastBackup);
  5846. addStyle(`
  5847. .backupreminder {
  5848. position:fixed;
  5849. height:auto;
  5850. overflow:auto;
  5851. top:110%;
  5852. left:40%;
  5853. z-index:200;
  5854. padding:5px;
  5855. transition: top 1s;
  5856. border:2px solid black;
  5857. border-radius:10px;
  5858. color:black;
  5859. background:white;
  5860. }
  5861. `);
  5862.  
  5863. // Blur background
  5864. if (document.getElementById('centerWrapper')) {
  5865. document.getElementById('centerWrapper').style.filter = 'blur(4px)';
  5866. }
  5867. const main = document.body.appendChild(document.createElement('div'));
  5868. main.className = 'backupreminder';
  5869. main.innerHTML = `<h2>${SCRIPT_NAME}</h2>
  5870. <h1>Backup reminder</h1>
  5871. <p>
  5872. Your last backup was ${since} ago. Since then, you played ${changedRecords} albums.
  5873. </p>
  5874. `;
  5875. main.appendChild(document.createElement('br'));
  5876. const buttons = main.appendChild(document.createElement('div'));
  5877. const closeButton = buttons.appendChild(document.createElement('button'));
  5878. closeButton.appendChild(document.createTextNode('Close'));
  5879. closeButton.id = 'backupreminder_close';
  5880. closeButton.style.color = 'black';
  5881. closeButton.addEventListener('click', function onCloseButtonClick() {
  5882. document.querySelector('.backupreminder').remove();
  5883. // Un-blur background
  5884. if (document.getElementById('centerWrapper')) {
  5885. document.getElementById('centerWrapper').style.filter = '';
  5886. }
  5887. });
  5888. buttons.appendChild(document.createTextNode(' '));
  5889. const backupButton = buttons.appendChild(document.createElement('button'));
  5890. backupButton.appendChild(document.createTextNode('Start backup'));
  5891. backupButton.style.color = '#0687f5';
  5892. backupButton.addEventListener('click', function backupButtonClick() {
  5893. document.getElementById('backupreminder_close').click();
  5894. mainMenu(true);
  5895. });
  5896. buttons.appendChild(document.createTextNode(' '));
  5897. const ignoreButton = buttons.appendChild(document.createElement('button'));
  5898. ignoreButton.appendChild(document.createTextNode('Disable reminder'));
  5899. ignoreButton.style.color = 'black';
  5900. ignoreButton.addEventListener('click', async function ignoreButtonClick() {
  5901. getEnabledFeatures(await GM.getValue('enabledFeatures', false));
  5902. if (allFeatures.backupReminder.enabled) {
  5903. allFeatures.backupReminder.enabled = false;
  5904. }
  5905. await GM.setValue('enabledFeatures', JSON.stringify(allFeatures));
  5906. document.getElementById('backupreminder_close').click();
  5907. });
  5908. window.setTimeout(function moveMenuIntoView() {
  5909. main.style.maxHeight = document.documentElement.clientHeight - 40 + 'px';
  5910. main.style.maxWidth = document.documentElement.clientWidth - 40 + 'px';
  5911. main.style.left = Math.max(20, 0.5 * (document.documentElement.clientWidth - main.clientWidth)) + 'px';
  5912. main.style.top = Math.max(20, 0.3 * document.documentElement.clientHeight) + 'px';
  5913. }, 0);
  5914. }
  5915. function downloadMp3FromLink(ev, a, addSpinner, removeSpinner, noGM) {
  5916. const url = a.href;
  5917. if (GM_download && !noGM) {
  5918. // Use Tampermonkey GM_download function
  5919. console.debug('Using GM_download function');
  5920. ev.preventDefault();
  5921. addSpinner(a);
  5922. let GMdownloadStatus = 0;
  5923. GM_download({
  5924. url,
  5925. name: a.download || 'default.mp3',
  5926. onerror: function downloadMp3FromLinkOnError(e) {
  5927. console.debug('GM_download onerror:', e);
  5928. window.setTimeout(function () {
  5929. if (GMdownloadStatus !== 1) {
  5930. if (url.startsWith('data')) {
  5931. console.debug('GM_download failed with data url');
  5932. document.location.href = url;
  5933. } else {
  5934. console.debug('Trying again with GM_download disabled');
  5935. downloadMp3FromLink(ev, a, addSpinner, removeSpinner, true);
  5936. }
  5937. }
  5938. }, 1000);
  5939. },
  5940. ontimeout: function downloadMp3FromLinkOnTimeout() {
  5941. window.alert('Could not download via GM_download. Time out.');
  5942. document.location.href = url;
  5943. },
  5944. onload: function downloadMp3FromLinkOnLoad() {
  5945. console.debug('Successfully downloaded via GM_download');
  5946. GMdownloadStatus = 1;
  5947. window.setTimeout(() => removeSpinner(a), 500);
  5948. }
  5949. });
  5950. return;
  5951. }
  5952. if (!url.startsWith('http') || navigator.userAgent.indexOf('Chrome') !== -1) {
  5953. // Just open the link normally (no prevent default)
  5954. addSpinner(a);
  5955. window.setTimeout(() => removeSpinner(a), 1000);
  5956. return;
  5957. }
  5958.  
  5959. // Use GM.xmlHttpRequest to download and offer data uri
  5960. ev.preventDefault();
  5961. console.debug('Using GM.xmlHttpRequest to download and then offer data uri');
  5962. addSpinner(a);
  5963. GM.xmlHttpRequest({
  5964. method: 'GET',
  5965. overrideMimeType: 'text/plain; charset=x-user-defined',
  5966. url,
  5967. onload: function onMp3Load(response) {
  5968. console.debug('Successfully received data via GM.xmlHttpRequest, starting download');
  5969. a.href = 'data:audio/mpeg;base64,' + base64encode(response.responseText);
  5970. window.setTimeout(() => a.click(), 10);
  5971. },
  5972. onerror: function onMp3LoadError(response) {
  5973. window.alert('Could not download via GM.xmlHttpRequest');
  5974. document.location.href = url;
  5975. }
  5976. });
  5977. }
  5978. function addDownloadLinksToAlbumPage() {
  5979. addStyle(`
  5980. .download-col .downloaddisk:hover {
  5981. text-decoration:none
  5982. }
  5983. /* From http://www.designcouch.com/home/why/2013/05/23/dead-simple-pure-css-loading-spinner/ */
  5984. .downspinner {
  5985. height:16px;
  5986. width:16px;
  5987. margin:0px auto;
  5988. position:relative;
  5989. display:inline-block;
  5990. animation: spinnerrotation 3s infinite linear;
  5991. cursor:wait;
  5992. }
  5993. @keyframes spinnerrotation {
  5994. from {transform: rotate(0deg)}
  5995. to {transform: rotate(359deg)}
  5996. }`);
  5997. const addSpiner = function downloadLinksOnAlbumPageAddSpinner(el) {
  5998. el.style = '';
  5999. el.classList.add('downspinner');
  6000. };
  6001. const removeSpinner = function downloadLinksOnAlbumPageRemoveSpinner(el) {
  6002. el.classList.remove('downspinner');
  6003. el.style = 'background:#1cea1c; border-radius:5px; padding:1px; opacity:0.5';
  6004. };
  6005. const TralbumData = unsafeWindow.TralbumData;
  6006. if (TralbumData && TralbumData.hasAudio && !TralbumData.freeDownloadPage && TralbumData.trackinfo) {
  6007. const hoverdiv = document.querySelectorAll('.download-col div');
  6008. if (hoverdiv.length > 0) {
  6009. // Album page
  6010. for (let i = 0; i < TralbumData.trackinfo.length; i++) {
  6011. if (!NOEMOJI && hoverdiv[i].querySelector('a[href*="?action=download"]')) {
  6012. // Replace buy link with shopping cart emoji
  6013. hoverdiv[i].querySelector('a[href*="?action=download"]').innerHTML = '&#x1f6d2;';
  6014. hoverdiv[i].querySelector('a[href*="?action=download"]').title = 'buy track';
  6015. }
  6016. // Add download link
  6017. const t = TralbumData.trackinfo[i];
  6018. if (!t.file) {
  6019. continue;
  6020. }
  6021. const prop = Object.keys(t.file)[0]; // Just use the first file entry
  6022. const mp3 = t.file[prop].replace(/^\/\//, 'http://');
  6023. const a = document.createElement('a');
  6024. a.className = 'downloaddisk';
  6025. a.href = mp3;
  6026. a.download = (t.track_num == null ? '' : (t.track_num > 9 ? '' : '0') + t.track_num + '. ') + fixFilename(TralbumData.artist + ' - ' + t.title) + '.mp3';
  6027. a.title = 'Download ' + prop;
  6028. a.appendChild(document.createTextNode(NOEMOJI ? '\u2193' : '\uD83D\uDCBE'));
  6029. a.addEventListener('click', function onDownloadLinkClick(ev) {
  6030. downloadMp3FromLink(ev, this, addSpiner, removeSpinner);
  6031. });
  6032. hoverdiv[i].appendChild(a);
  6033. }
  6034. } else if (document.querySelector('#trackInfo .download-link')) {
  6035. // Single track page
  6036. const t = TralbumData.trackinfo[0];
  6037. if (!t.file) {
  6038. return;
  6039. }
  6040. const prop = Object.keys(t.file)[0];
  6041. const mp3 = t.file[prop].replace(/^\/\//, 'http://');
  6042. const a = document.createElement('a');
  6043. a.className = 'downloaddisk';
  6044. a.href = mp3;
  6045. a.download = (t.track_num == null ? '' : (t.track_num > 9 ? '' : '0') + t.track_num + '. ') + fixFilename(TralbumData.artist + ' - ' + t.title) + '.mp3';
  6046. a.title = 'Download ' + prop;
  6047. a.appendChild(document.createTextNode(NOEMOJI ? '\u2193' : '\uD83D\uDCBE'));
  6048. a.addEventListener('click', function onDownloadLinkClick(ev) {
  6049. downloadMp3FromLink(ev, this, addSpiner, removeSpinner);
  6050. });
  6051. document.querySelector('#trackInfo .download-link').parentNode.appendChild(a);
  6052. }
  6053. }
  6054. }
  6055. function addOpenDiscographyPlayerFromAlbumPage() {
  6056. // Open discrography player by clicking on top right corner of album art
  6057. // Shows the usual play button on hover
  6058. const xRatio = 0.7;
  6059. const yRatio = 0.3;
  6060. let rect = null;
  6061. const isInRatio = function isInRatio(ev) {
  6062. rect = rect || ev.target.getBoundingClientRect();
  6063. const x = ev.clientX - rect.left;
  6064. const y = ev.clientY - rect.top;
  6065. return x > rect.width * xRatio && y < rect.height * yRatio;
  6066. };
  6067. const a = document.querySelector('#tralbumArt a.popupImage');
  6068. if (!a) {
  6069. return;
  6070. }
  6071. const div = a.appendChild(document.createElement('div'));
  6072. div.classList.add('art-play');
  6073. div.innerHTML = '<div class="art-play-bg"></div><div class="art-play-icon"></div>';
  6074. a.classList.add('playFromAlbumPage');
  6075. addStyle(`
  6076. .playFromAlbumPage .art-play {
  6077. position: absolute;
  6078. width: 74px;
  6079. height: 54px;
  6080. right: 7%;
  6081. top: 15%;
  6082. margin-left: -36px;
  6083. margin-top: -27px;
  6084. opacity: 0.0;
  6085. transition: opacity 0.2s;
  6086. }
  6087. .playFromAlbumPage .art-play-bg {
  6088. position: absolute;
  6089. width: 100%;
  6090. height: 100%;
  6091. left: 0;
  6092. top: 0;
  6093. background: #000;
  6094. border-radius: 4px;
  6095. }
  6096. .playFromAlbumPage .art-play-icon {
  6097. position: absolute;
  6098. width: 0;
  6099. height: 0;
  6100. left: 28px;
  6101. top: 17px;
  6102. border-width: 10px 0 10px 17px;
  6103. border-color: transparent transparent transparent #fff;
  6104. border-style: dashed dashed dashed solid;
  6105. }
  6106. `);
  6107. a.addEventListener('click', function onAlbumArtClick(ev) {
  6108. if (isInRatio(ev)) {
  6109. // Open player
  6110. ev.preventDefault();
  6111. ev.stopPropagation();
  6112. if (unsafeWindow.TralbumData) {
  6113. addAlbumToPlaylist(unsafeWindow.TralbumData);
  6114. } else {
  6115. playAlbumFromUrl(document.location.href);
  6116. }
  6117. }
  6118. }, true);
  6119. a.addEventListener('mouseover', function onAlbumArtOver(ev) {
  6120. if (isInRatio(ev)) {
  6121. a.querySelector('.art-play').style.opacity = 0.7;
  6122. } else {
  6123. a.querySelector('.art-play').style.opacity = 0.0;
  6124. }
  6125. });
  6126. a.addEventListener('mousemove', function onAlbumArtOver(ev) {
  6127. if (isInRatio(ev)) {
  6128. a.querySelector('.art-play').style.opacity = 0.7;
  6129. } else {
  6130. a.querySelector('.art-play').style.opacity = 0.0;
  6131. }
  6132. });
  6133. a.addEventListener('mouseleave', function onAlbumArtOver(ev) {
  6134. rect = null;
  6135. a.querySelector('.art-play').style.opacity = 0.0;
  6136. });
  6137. }
  6138. function addLyricsToAlbumPage() {
  6139. // Load lyrics from html into TralbumData
  6140. const TralbumData = unsafeWindow.TralbumData;
  6141. function findInTralbumData(url) {
  6142. for (let i = 0; i < TralbumData.trackinfo.length; i++) {
  6143. const t = TralbumData.trackinfo[i];
  6144. if (url.endsWith(t.title_link)) {
  6145. return t;
  6146. }
  6147. }
  6148. return null;
  6149. }
  6150. const tracks = Array.from(document.querySelectorAll('#track_table .track_row_view .title a')).map(a => findInTralbumData(a.href));
  6151. document.querySelectorAll('#track_table .track_row_view .title a').forEach(function (a) {
  6152. const tr = parentQuery(a, 'tr[rel]');
  6153. const trackNum = tr.getAttribute('rel').split('tracknum=')[1];
  6154. const lyricsRow = document.querySelector('#track_table tr#lyrics_row_' + trackNum);
  6155. const lyricsLink = tr.querySelector('.geniuslink');
  6156. if (tr.querySelector('.info_link').innerHTML.indexOf('lyrics') === -1) {
  6157. // Hide info link if there are no lyrics
  6158. tr.querySelector('.info_link a[href*="/track/"]').innerHTML = '';
  6159. }
  6160. if (lyricsRow) {
  6161. const trackNum = parseInt(lyricsRow.id.split('lyrics_row_')[1]);
  6162. for (let i = 0; i < tracks.length; i++) {
  6163. if (trackNum === tracks[i].track_num) {
  6164. tracks[i].lyrics = lyricsRow.querySelector('div').textContent;
  6165. }
  6166. }
  6167. } else if (!lyricsLink) {
  6168. // Add genius link
  6169. const lyricsLink = tr.querySelector('.info_link').appendChild(document.createElement('a'));
  6170. lyricsLink.dataset.trackNum = trackNum;
  6171. lyricsLink.title = 'load lyrics from genius.com';
  6172. lyricsLink.href = '#geniuslyrics-' + trackNum;
  6173. lyricsLink.classList.add('geniuslink');
  6174. lyricsLink.appendChild(document.createTextNode('G'));
  6175. lyricsLink.style = 'color: black;background: rgb(255, 255, 100);border-radius: 50%;padding: 0px 3px;border: 1px solid black';
  6176. lyricsLink.addEventListener('click', function () {
  6177. loadGeniusLyrics(parseInt(this.dataset.trackNum));
  6178. });
  6179. }
  6180. });
  6181. }
  6182. let genius = null;
  6183. let geniusContainerTr = null;
  6184. let geniusTrackNum = -1;
  6185. let geniusArtistsArr = [];
  6186. let geniusTitle = '';
  6187. function geniusGetCleanLyricsContainer() {
  6188. geniusContainerTr.innerHTML = `
  6189. <td colspan="5">
  6190. <div></div>
  6191. </td>
  6192. `;
  6193. return geniusContainerTr.querySelector('div');
  6194. }
  6195. function geniusAddLyrics(force, beLessSpecific) {
  6196. genius.f.loadLyrics(force, beLessSpecific, geniusTitle, geniusArtistsArr, true);
  6197. }
  6198. function geniusHideLyrics() {
  6199. document.querySelectorAll('.loadingspinner').forEach(spinner => spinner.remove());
  6200. document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'));
  6201. }
  6202. function geniusSetFrameDimensions(container, iframe) {
  6203. const width = iframe.style.width = '500px';
  6204. const height = iframe.style.height = '650px';
  6205. if (genius.option.themeKey === 'spotify') {
  6206. iframe.style.backgroundColor = 'black';
  6207. } else {
  6208. iframe.style.backgroundColor = '';
  6209. }
  6210. return [width, height];
  6211. }
  6212. function geniusAddCss() {
  6213. addStyle(geniusCSS);
  6214. addStyle(`
  6215. #myconfigwin39457845 {
  6216. background-color:${darkModeModeCurrent === true ? '#a2a2a2' : 'white'} !important;
  6217. color:${darkModeModeCurrent === true ? 'white' : 'black'} !important;
  6218. }
  6219. #myconfigwin39457845 div {
  6220. background-color:${darkModeModeCurrent === true ? '#3E3E3E' : '#EFEFEF'} !important
  6221. }
  6222. .lyricsnavbar {
  6223. background:${darkModeModeCurrent === true ? '#7d7c7c' : '#fafafa'} !important;
  6224. }
  6225. `);
  6226. }
  6227. function geniusCreateSpinner(spinnerHolder) {
  6228. geniusContainerTr.querySelector('div').insertBefore(spinnerHolder, geniusContainerTr.querySelector('div').firstChild);
  6229. const spinner = spinnerHolder.appendChild(document.createElement('div'));
  6230. spinner.classList.add('loadingspinner');
  6231. return spinner;
  6232. }
  6233. function geniusShowSearchField(query) {
  6234. const b = geniusGetCleanLyricsContainer();
  6235. b.style.border = '1px solid black';
  6236. b.style.borderRadius = '3px';
  6237. b.style.padding = '5px';
  6238. b.appendChild(document.createTextNode('Search genius.com: '));
  6239. b.style.paddingRight = '15px';
  6240. const input = b.appendChild(document.createElement('input'));
  6241. input.className = 'SearchInputBox__input';
  6242. input.placeholder = 'Search genius.com...';
  6243. input.style = 'width: 300px;background-color: #F3F3F3;padding: 10px 30px 10px 10px;font-size: 14px; border: none;color: #333;margin: 6px 0;height: 17px;border-radius: 3px;';
  6244. const span = b.appendChild(document.createElement('span'));
  6245. span.style = 'cursor:pointer; margin-left: -25px;';
  6246. span.appendChild(document.createTextNode(' \uD83D\uDD0D'));
  6247. if (query) {
  6248. input.value = query;
  6249. } else if (genius.current.artists) {
  6250. input.value = genius.current.artists;
  6251. }
  6252. input.addEventListener('change', function onSearchLyricsButtonClick() {
  6253. if (input.value) {
  6254. genius.f.searchByQuery(input.value, b);
  6255. }
  6256. });
  6257. input.addEventListener('keyup', function onSearchLyricsKeyUp(ev) {
  6258. if (ev.keyCode === 13) {
  6259. ev.preventDefault();
  6260. if (input.value) {
  6261. genius.f.searchByQuery(input.value, b);
  6262. }
  6263. }
  6264. });
  6265. span.addEventListener('click', function onSearchLyricsKeyUp(ev) {
  6266. if (input.value) {
  6267. genius.f.searchByQuery(input.value, b);
  6268. }
  6269. });
  6270. input.focus();
  6271. }
  6272. function geniusListSongs(hits, container, query) {
  6273. if (!container) {
  6274. container = geniusGetCleanLyricsContainer();
  6275. }
  6276.  
  6277. // Back to search button
  6278. const backToSearchButton = document.createElement('a');
  6279. backToSearchButton.href = '#';
  6280. backToSearchButton.appendChild(document.createTextNode('Back to search'));
  6281. backToSearchButton.addEventListener('click', function backToSearchButtonClick(ev) {
  6282. ev.preventDefault();
  6283. if (query) {
  6284. geniusShowSearchField(query);
  6285. } else if (genius.current.artists) {
  6286. geniusShowSearchField(genius.current.artists + ' ' + genius.current.title);
  6287. } else {
  6288. geniusShowSearchField();
  6289. }
  6290. });
  6291. const separator = document.createElement('span');
  6292. separator.setAttribute('class', 'second-line-separator');
  6293. separator.setAttribute('style', 'padding:0px 3px');
  6294. separator.appendChild(document.createTextNode('•'));
  6295.  
  6296. // Hide button
  6297. const hideButton = document.createElement('a');
  6298. hideButton.href = '#';
  6299. hideButton.appendChild(document.createTextNode('Hide'));
  6300. hideButton.addEventListener('click', function hideButtonClick(ev) {
  6301. ev.preventDefault();
  6302. geniusHideLyrics();
  6303. });
  6304.  
  6305. // List search results
  6306. const trackhtml = '<div style="float:left;"><div class="onhover" style="margin-top:-0.25em;display:none"><span style="color:black;font-size:2.0em">🅖</span></div><div class="onout"><span style="font-size:1.5em">📄</span></div></div>' + '<div style="float:left; margin-left:5px">$artist • $title <br><span style="font-size:0.7em">👁 $stats.pageviews $lyrics_state</span></div><div style="clear:left;"></div>';
  6307. container.innerHTML = '<ol class="tracklist" style="font-size:1.15em"></ol>';
  6308. container.classList.add('searchresultlist');
  6309. if (darkModeModeCurrent === true) {
  6310. container.style.backgroundColor = '#262626';
  6311. container.style.position = 'relative';
  6312. }
  6313. container.insertBefore(hideButton, container.firstChild);
  6314. container.insertBefore(separator, container.firstChild);
  6315. container.insertBefore(backToSearchButton, container.firstChild);
  6316. const ol = container.querySelector('ol');
  6317. const searchresultsLengths = hits.length;
  6318. const title = genius.current.title;
  6319. const artists = genius.current.artists;
  6320. const onclick = function onclick() {
  6321. genius.f.rememberLyricsSelection(title, artists, this.dataset.hit);
  6322. genius.f.showLyrics(JSON.parse(this.dataset.hit), searchresultsLengths);
  6323. };
  6324. const mouseover = function onmouseover() {
  6325. this.querySelector('.onhover').style.display = 'block';
  6326. this.querySelector('.onout').style.display = 'none';
  6327. this.style.backgroundColor = darkModeModeCurrent === true ? 'rgb(70, 70, 70)' : 'rgb(200, 200, 200)';
  6328. };
  6329. const mouseout = function onmouseout() {
  6330. this.querySelector('.onhover').style.display = 'none';
  6331. this.querySelector('.onout').style.display = 'block';
  6332. this.style.backgroundColor = darkModeModeCurrent === true ? '#262626' : 'rgb(255, 255, 255)';
  6333. };
  6334. hits.forEach(function forEachHit(hit) {
  6335. const li = document.createElement('li');
  6336. if (darkModeModeCurrent === true) {
  6337. li.style.backgroundColor = '#262626';
  6338. }
  6339. li.style.cursor = 'pointer';
  6340. li.style.transition = 'background-color 0.2s';
  6341. li.style.padding = '3px';
  6342. li.style.margin = '2px';
  6343. li.style.borderRadius = '3px';
  6344. li.innerHTML = trackhtml.replace(/\$title/g, hit.result.title_with_featured).replace(/\$artist/g, hit.result.primary_artist.name).replace(/\$lyrics_state/g, hit.result.lyrics_state).replace(/\$stats\.pageviews/g, genius.f.metricPrefix(hit.result.stats.pageviews, 1));
  6345. li.dataset.hit = JSON.stringify(hit);
  6346. li.addEventListener('click', onclick);
  6347. li.addEventListener('mouseover', mouseover);
  6348. li.addEventListener('mouseout', mouseout);
  6349. ol.appendChild(li);
  6350. });
  6351. }
  6352. function geniusOnLyricsReady(song, container) {
  6353. container.parentNode.parentNode.dataset.loaded = 'loaded';
  6354. }
  6355. function geniusOnNoResults(songTitle, songArtistsArr) {
  6356. geniusContainerTr.dataset.loaded = 'loaded';
  6357. document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'));
  6358. document.querySelector(`#track_table tr[rel="tracknum=${geniusTrackNum}"]`).classList.add('showlyrics');
  6359. geniusShowSearchField(songArtistsArr.join(' ') + ' ' + songTitle);
  6360. }
  6361. let geniusAudio = null;
  6362. let geniusLastPos = null;
  6363. function geniusAudioTimeUpdate() {
  6364. if (!geniusAudio) {
  6365. geniusAudio = document.querySelector('body>audio[src]');
  6366. }
  6367. if (!geniusAudio) {
  6368. return;
  6369. }
  6370. const pos = geniusAudio.currentTime / geniusAudio.duration;
  6371. if (pos >= 0 && `${geniusLastPos}` !== `${pos}`) {
  6372. geniusLastPos = pos;
  6373. genius.f.scrollLyrics(pos);
  6374. }
  6375. }
  6376. function initGenius() {
  6377. if (!genius) {
  6378. genius = geniusLyrics({
  6379. GM: {
  6380. xmlHttpRequest: GM.xmlHttpRequest,
  6381. getValue: (name, defaultValue) => GM.getValue('genius_' + name, defaultValue),
  6382. setValue: (name, value) => GM.setValue('genius_' + name, value)
  6383. },
  6384. scriptName: SCRIPT_NAME,
  6385. scriptIssuesURL: 'https://github.com/cvzi/Bandcamp-script-deluxe-edition/issues',
  6386. scriptIssuesTitle: 'Report problem: github.com/cvzi/Bandcamp-script-deluxe-edition/issues',
  6387. domain: document.location.origin + '/',
  6388. emptyURL: document.location.origin + LYRICS_EMPTY_PATH,
  6389. addCss: geniusAddCss,
  6390. listSongs: geniusListSongs,
  6391. showSearchField: geniusShowSearchField,
  6392. addLyrics: geniusAddLyrics,
  6393. hideLyrics: geniusHideLyrics,
  6394. getCleanLyricsContainer: geniusGetCleanLyricsContainer,
  6395. setFrameDimensions: geniusSetFrameDimensions,
  6396. createSpinner: geniusCreateSpinner,
  6397. onLyricsReady: geniusOnLyricsReady,
  6398. onNoResults: geniusOnNoResults
  6399. });
  6400. document.addEventListener('timeupdate', geniusAudioTimeUpdate, true);
  6401. }
  6402. }
  6403. function loadGeniusLyrics(trackNum) {
  6404. // Toggle lyrics
  6405. geniusContainerTr = document.getElementById('lyrics_row_' + trackNum);
  6406. let tr;
  6407. if (geniusContainerTr) {
  6408. tr = document.querySelector(`#track_table tr[rel="tracknum=${trackNum}"]`);
  6409. if ('loaded' in geniusContainerTr.dataset && geniusContainerTr.dataset.loaded === 'loaded') {
  6410. if (tr.classList.contains('showlyrics')) {
  6411. // Hide lyrics if already loaded
  6412. document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'));
  6413. } else {
  6414. // Show lyrics again
  6415. document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'));
  6416. tr.classList.add('showlyrics');
  6417. }
  6418. return;
  6419. } else if (geniusTrackNum === trackNum) {
  6420. // Lyrics currently loading
  6421. console.debug('loadGeniusLyrics already loading trackNum=' + trackNum);
  6422. return;
  6423. }
  6424. }
  6425. geniusTrackNum = trackNum;
  6426. if (!geniusContainerTr) {
  6427. geniusContainerTr = document.createElement('tr');
  6428. geniusContainerTr.className = 'lyricsRow';
  6429. geniusContainerTr.setAttribute('id', 'lyrics_row_' + trackNum);
  6430. tr = document.querySelector(`#track_table tr[rel="tracknum=${trackNum}"]`);
  6431. if (tr.nextElementSibling) {
  6432. tr.parentNode.insertBefore(geniusContainerTr, tr.nextElementSibling);
  6433. } else {
  6434. tr.parentNode.appendChild(geniusContainerTr);
  6435. }
  6436. document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'));
  6437. tr.classList.add('showlyrics');
  6438. const spinnerHolder = geniusContainerTr.appendChild(document.createElement('div'));
  6439. spinnerHolder.classList.add('loadingspinnerholder');
  6440. const spinner = spinnerHolder.appendChild(document.createElement('div'));
  6441. spinner.classList.add('loadingspinner');
  6442. }
  6443. initGenius();
  6444. const track = unsafeWindow.TralbumData.trackinfo.find(t => t.track_num === trackNum);
  6445. geniusTitle = track.title;
  6446. geniusArtistsArr = unsafeWindow.TralbumData.artist.split(/&|,|ft\.?|feat\.?/).map(s => s.trim());
  6447. geniusAddLyrics();
  6448. }
  6449. let explorer = null;
  6450. async function showExplorer() {
  6451. if (explorer) {
  6452. explorer.style.display = 'block';
  6453. return explorer;
  6454. }
  6455. document.title = 'Explorer';
  6456. document.body.innerHTML = '';
  6457. explorer = document.body.appendChild(document.createElement('div'));
  6458. explorer.setAttribute('id', 'expRoot');
  6459. addStyle(`
  6460. #expRoot {
  6461. background:white;
  6462. color:black
  6463. }
  6464. #expRoot .albumListItem{
  6465. cursor:pointer;
  6466. background:#ddd;
  6467. display: flex;
  6468. align-items: center;
  6469. justify-content: center;
  6470. }
  6471. #expRoot .albumListItemOdd{
  6472. background:#eee
  6473. }
  6474. #expRoot .albumListItem:hover{
  6475. background:greenyellow
  6476. }
  6477.  
  6478. #expRoot .albumListItem.selected{
  6479. background:#aaa;
  6480. }
  6481.  
  6482. `);
  6483. new Explorer(document.getElementById('expRoot'), {
  6484. playAlbumFromUrl,
  6485. deletePermanentTralbum
  6486. }).render();
  6487. }
  6488. function appendMainMenuButtonTo(ul) {
  6489. addStyle(`
  6490. .menubar-item .menubar-symbol {
  6491. display:flex;
  6492. font-size:24px !important;
  6493. transition:transform 1s ease-out
  6494. }
  6495. .menubar-item .menubar-symbol:hover {
  6496. text-decoration:none
  6497. }
  6498. .menubar-item:hover .menubar-symbol-settings {
  6499. transform:rotate(1turn)
  6500. }
  6501. .menubar-item:hover .menubar-symbol-library {
  6502. transform:scale(-1, 1)
  6503. }
  6504. .menubar-item:hover .menubar-symbol-tag-search {
  6505. transform:scale(1.3)
  6506. }
  6507. `);
  6508. const liSettings = ul.insertBefore(document.createElement('li'), ul.firstChild);
  6509. liSettings.className = 'menubar-item hoverable';
  6510. liSettings.title = 'userscript settings - ' + SCRIPT_NAME;
  6511. const aSettings = liSettings.appendChild(document.createElement('a'));
  6512. aSettings.className = 'menubar-symbol menubar-symbol-settings';
  6513. aSettings.href = '#';
  6514. if (NOEMOJI) {
  6515. const img = aSettings.appendChild(document.createElement('img'));
  6516. img.style = 'display:inline; width:34px; vertical-align:middle;';
  6517. img.src = 'https://raw.githubusercontent.com/hfg-gmuend/openmoji/master/color/72x72/2699.png';
  6518. } else {
  6519. aSettings.appendChild(document.createTextNode('\u2699\uFE0F'));
  6520. }
  6521. liSettings.addEventListener('click', () => mainMenu());
  6522. if (allFeatures.keepLibrary.enabled) {
  6523. const liExplorer = ul.insertBefore(document.createElement('li'), ul.firstChild);
  6524. liExplorer.className = 'menubar-item hoverable';
  6525. liExplorer.title = 'library - ' + SCRIPT_NAME;
  6526. const aExplorer = liExplorer.appendChild(document.createElement('a'));
  6527. aExplorer.className = 'menubar-symbol menubar-symbol-library';
  6528. aExplorer.href = PLAYER_URL;
  6529. if (NOEMOJI) {
  6530. const img = aExplorer.appendChild(document.createElement('img'));
  6531. img.style = 'display:inline; width:34px; vertical-align:middle;';
  6532. img.src = 'https://raw.githubusercontent.com/hfg-gmuend/openmoji/master/color/72x72/1F5C3.png';
  6533. } else {
  6534. aExplorer.appendChild(document.createTextNode('\uD83D\uDDC3\uFE0F'));
  6535. }
  6536. aExplorer.target = '_blank';
  6537. // TODO open library in frame
  6538. // liExplorer.addEventListener('click', function (ev) {
  6539. // ev.preventDefault()
  6540. // openExplorer()
  6541. // })
  6542. }
  6543.  
  6544. const liSearch = ul.insertBefore(document.createElement('li'), ul.firstChild);
  6545. liSearch.className = 'menubar-item hoverable menubar-item-tag-search';
  6546. liSearch.title = 'tag search - ' + SCRIPT_NAME;
  6547. const aSearch = liSearch.appendChild(document.createElement('a'));
  6548. aSearch.className = 'menubar-symbol menubar-symbol-tag-search';
  6549. aSearch.href = '#';
  6550. if (NOEMOJI) {
  6551. aSearch.innerHTML = `
  6552. <svg width="22" height="22" viewBox="0 0 15 16" class="svg-icon" style="border: 2px solid #000000c4;border-radius: 30%;padding: 3px;">
  6553. <use xlink:href="#menubar-search-input-icon">
  6554. </svg>`;
  6555. } else {
  6556. aSearch.appendChild(document.createTextNode('\uD83D\uDD0D'));
  6557. }
  6558. aSearch.setAttribute('id', 'bcsde_tagsearchbutton');
  6559. aSearch.addEventListener('click', showTagSearchForm);
  6560. }
  6561. function appendMainMenuButtonLeftTo(leftOf) {
  6562. // Wait for the design to load images
  6563. window.setTimeout(() => {
  6564. const rect = leftOf.getBoundingClientRect();
  6565. const ul = document.createElement('ul');
  6566. ul.className = 'bcsde_settingsbar';
  6567. appendMainMenuButtonTo(document.body.appendChild(ul));
  6568. addStyle(`
  6569. .bcsde_settingsbar {position:absolute; top:-15px; left:${rect.right}px; list-style-type: none; padding:0; margin:0; opacity:0.6; transition:top 300ms}
  6570. .bcsde_settingsbar:hover {top:${rect.top}px}
  6571. .bcsde_settingsbar a:hover {text-decoration:none}
  6572. .bcsde_settingsbar li {float:left; padding:0; margin:0}`);
  6573. window.addEventListener('resize', function () {
  6574. ul.style.left = leftOf.getBoundingClientRect().right + 'px';
  6575. });
  6576. }, 500);
  6577. }
  6578. function humour() {
  6579. if (document.getElementById('salesfeed')) {
  6580. const salesfeedHumour = {};
  6581. salesfeedHumour.all = [`${SCRIPT_NAME} by cuzi, Dark theme by Simonus`, `Provide feedback for ${SCRIPT_NAME} on openuser.js or github.com`, `${SCRIPT_NAME} - nobody pays for software anymore 🙌🏽`];
  6582. salesfeedHumour.chosen = salesfeedHumour.all[0];
  6583. unsafeWindow.$('#pagedata').data('blob').salesfeed_humour = salesfeedHumour;
  6584. }
  6585. }
  6586. function showAlbumID() {
  6587. if (unsafeWindow.TralbumData && 'id' in unsafeWindow.TralbumData && document.querySelector('#name-section h3')) {
  6588. document.querySelectorAll('#name-section h3').forEach(function (h3) {
  6589. const id = unsafeWindow.TralbumData.id;
  6590. const h4 = h3.parentNode.appendChild(document.createElement('h4'));
  6591. h4.style.fontSize = '13px';
  6592. h4.style.fontWeight = 'normal';
  6593. h4.style.opacity = 0.6;
  6594. h4.style.marginTop = '4px';
  6595. h4.innerHTML = `Album ID: <span style="user-select: all;">${id}</span>`;
  6596. h4.addEventListener('click', function () {
  6597. GM_setClipboard(id.toString());
  6598. const span = h4.appendChild(document.createElement('span'));
  6599. span.innerHTML = ' copied!';
  6600. span.style.marginLeft = '5px';
  6601. span.style.transition = 'opacity 2s';
  6602. span.style.opacity = 1;
  6603. window.setInterval(() => span.style.opacity = 0, 0);
  6604. window.setInterval(() => span.remove(), 1000);
  6605. });
  6606. });
  6607. }
  6608. }
  6609. function formatReleaseDateOnAlbumPage() {
  6610. const textContainers = document.querySelectorAll('.tralbumData');
  6611. if (textContainers.length === 0) {
  6612. return;
  6613. }
  6614. GM.getValue('custom_release_date_format_str').then(function customFormatReleaseDate(format) {
  6615. if (!format || !format.trim()) {
  6616. console.warn('formatReleaseDateOnAlbumPage: No custom release date format string set.');
  6617. return;
  6618. }
  6619. textContainers.forEach(function (textContainer) {
  6620. for (const match of textContainer.innerHTML.matchAll(/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s+(\d{4})/gim)) {
  6621. const epochMs = Date.parse(match[0]);
  6622. if (Number.isNaN(epochMs)) {
  6623. console.warn(`formatReleaseDateOnAlbumPage: Could not parse date string "${match[0].trim()}"`);
  6624. continue;
  6625. }
  6626. const date = new Date(epochMs);
  6627. textContainer.innerHTML = textContainer.innerHTML.replace(match[0], `${customDateFormatter(format, date)}`);
  6628. }
  6629. });
  6630. });
  6631. }
  6632. function showDownloadLinkOnAlbumPage() {
  6633. if (!document.querySelector('a[href*="purchases?from=menubar"]')) {
  6634. return;
  6635. }
  6636. const purchasesUrl = document.querySelector('a[href*="purchases?from=menubar"]').href;
  6637. const itemUrl = document.location.href.split('#')[0];
  6638. const showDownloadLinkForUrl = function (downloadUrl) {
  6639. const purchasedMsgA = document.querySelector('#purchased-msg a');
  6640. purchasedMsgA.href = downloadUrl;
  6641. purchasedMsgA.textContent = 'Download';
  6642. };
  6643. GM.xmlHttpRequest({
  6644. method: 'GET',
  6645. url: purchasesUrl,
  6646. onload: function loadPurchases(response) {
  6647. const doc = new window.DOMParser().parseFromString(response.responseText, 'text/html').documentElement;
  6648. for (const purchasesItem of Array.from(doc.querySelectorAll('.purchases-item'))) {
  6649. if (!purchasesItem.querySelector('.purchases-item-title[href]')) {
  6650. continue;
  6651. }
  6652. const url = purchasesItem.querySelector('.purchases-item-title[href]').href;
  6653. if (url !== itemUrl) {
  6654. continue;
  6655. }
  6656. const downloadLink = purchasesItem.querySelector('.purchases-item-download a[href]');
  6657. if (!downloadLink && !downloadLink.href) {
  6658. continue;
  6659. }
  6660. return showDownloadLinkForUrl(downloadLink.href);
  6661. }
  6662. if (doc.querySelector('#js-crumbs-data') && doc.querySelector('#pagedata')) {
  6663. try {
  6664. const crumb = JSON.parse(doc.querySelector('#js-crumbs-data').dataset.crumbs)['api/orderhistory/1/get_items'];
  6665. const orderhistory = JSON.parse(doc.querySelector('#pagedata').dataset.blob).orderhistory;
  6666. nextOrderHistoryPage(itemUrl, {
  6667. username: orderhistory.username,
  6668. last_token: orderhistory.last_token,
  6669. platform: orderhistory.platform,
  6670. crumb
  6671. }, showDownloadLinkForUrl);
  6672. } catch (e) {
  6673. console.error('Error in showDownloadLinkOnAlbumPage, failed to launch nextOrderHistoryPage:', e);
  6674. }
  6675. }
  6676. },
  6677. onerror: function loadPurchasesError(response) {
  6678. console.error('showDownloadLinkOnAlbumPage() in onerror() Error: ' + response.status + '\nResponse:\n' + response.responseText + '\n' + ('error' in response ? response.error : ''));
  6679. }
  6680. });
  6681. }
  6682. async function nextOrderHistoryPage(itemUrl, data, cbFoundDownloadLink) {
  6683. // Load download links from api (same as clicking on "more" at the bototm of purchases page)
  6684. const handleResponse = function (result) {
  6685. for (const item of result.items) {
  6686. if (item.item_url === itemUrl) {
  6687. return cbFoundDownloadLink(item.download_url);
  6688. }
  6689. }
  6690. if ('last_token' in result && result.last_token) {
  6691. data.last_token = result.last_token;
  6692. return nextOrderHistoryPage(itemUrl, data, cbFoundDownloadLink);
  6693. }
  6694. };
  6695. const cacheKey = data.last_token;
  6696. const cached = await cacheGet('orderhistory', ONEHOUR, cacheKey, null);
  6697. if (cached) {
  6698. return handleResponse(JSON.parse(cached));
  6699. }
  6700. GM.xmlHttpRequest({
  6701. method: 'POST',
  6702. url: 'https://bandcamp.com/api/orderhistory/1/get_items',
  6703. headers: {
  6704. ' Content-Type': 'application/json'
  6705. },
  6706. data: JSON.stringify(data),
  6707. onload: function loadPurchases(response) {
  6708. try {
  6709. const result = JSON.parse(response.responseText);
  6710. cacheSet('orderhistory', ONEHOUR, cacheKey, response.responseText);
  6711. handleResponse(result);
  6712. } catch (e) {
  6713. console.error('Error in nextOrderHistoryPage:', e);
  6714. }
  6715. },
  6716. onerror: function loadPurchasesError(response) {
  6717. console.error('nextOrderHistoryPage () in onerror() Error: ' + response.status + '\nResponse:\n' + response.responseText + '\n' + ('error' in response ? response.error : ''));
  6718. }
  6719. });
  6720. }
  6721. function feedShowOnlyNewReleases() {
  6722. const stories = document.querySelectorAll('#stories li.story');
  6723. if (stories.length < 0) {
  6724. window.setTimeout(feedShowOnlyNewReleases, 10000);
  6725. return;
  6726. }
  6727. if (Array.from(stories).reduce((accumulator, story) => {
  6728. // Remove stories that are not 'nr' => new releases
  6729. if (!story.classList.contains('nr')) {
  6730. story.remove();
  6731. accumulator++;
  6732. }
  6733. return accumulator;
  6734. }, 0)) {
  6735. // If any were removed, trigger a reload of the feed
  6736. window.scrollBy(0, 1);
  6737. window.scrollBy(0, -1);
  6738. window.setTimeout(feedShowOnlyNewReleases, 500);
  6739. } else {
  6740. window.setTimeout(feedShowOnlyNewReleases, 1500);
  6741. }
  6742. }
  6743. function feedAddAudioControls() {
  6744. const colors = {
  6745. chrome: {
  6746. light: {
  6747. button_bg: 'white',
  6748. audio_bg: '',
  6749. audio_opacity: 1.0,
  6750. div_bg: '#f1f3f4',
  6751. div_border: '1px solid black'
  6752. },
  6753. dark: {
  6754. button_bg: '#797a7a',
  6755. audio_bg: 'black',
  6756. audio_opacity: 0.5,
  6757. div_bg: 'black',
  6758. div_border: 'none'
  6759. }
  6760. },
  6761. firefox: {
  6762. light: {
  6763. button_bg: 'white',
  6764. audio_bg: '#FFFF',
  6765. audio_opacity: 1.0,
  6766. div_bg: '#474747',
  6767. div_border: '3px solid white'
  6768. },
  6769. dark: {
  6770. button_bg: '#797a7a',
  6771. audio_bg: 'black',
  6772. audio_opacity: 1.0,
  6773. div_bg: '#151515',
  6774. div_border: 'none'
  6775. }
  6776. }
  6777. };
  6778. const play = function (ev) {
  6779. ev.preventDefault();
  6780. playAlbumFromUrl(document.querySelector('.story-list .collection-item-container.playing a.item-link').href);
  6781. };
  6782. const goTo = function (ev) {
  6783. ev.preventDefault();
  6784. document.querySelector('.story-list .collection-item-container.playing').scrollIntoView();
  6785. };
  6786. const open = function (ev) {
  6787. ev.preventDefault();
  6788. document.querySelector('.story-list .collection-item-container.playing a.item-link').click();
  6789. };
  6790. const next = function (ev) {
  6791. ev.preventDefault();
  6792. feedPlayNextItem();
  6793. };
  6794. const wishList = function (ev) {
  6795. ev.preventDefault();
  6796. window.open(document.querySelector('.story-list .collection-item-container.playing a.item-link').href + '#collect-wishlist');
  6797. };
  6798. const makeAudioVisible = function () {
  6799. const currentStyle = (CHROME ? colors.chrome : colors.firefox)[darkModeModeCurrent === true ? 'dark' : 'light'];
  6800. const audio = this;
  6801. audio.removeEventListener('timeupdate', makeAudioVisible);
  6802. audio.controls = true;
  6803. audio.loop = false;
  6804. const aStyle = `display:inline-block; background:${currentStyle.button_bg}; margin: 1px 1em; padding: 2px; border-radius: 4px;`;
  6805. const div = audio.parentNode.appendChild(document.createElement('div'));
  6806. const div2 = div.appendChild(document.createElement('div'));
  6807. const aPlay = div2.appendChild(document.createElement('a'));
  6808. aPlay.href = '#';
  6809. aPlay.addEventListener('click', play);
  6810. aPlay.style = aStyle;
  6811. const img = aPlay.appendChild(document.createElement('img'));
  6812. img.src = 'https://raw.githubusercontent.com/cvzi/Bandcamp-script-deluxe-edition/master/images/icon.png';
  6813. img.style = 'width: 14px; vertical-align: sub;padding:0px 3px 0px 0px;';
  6814. img.alt = 'Play in discography player';
  6815. aPlay.appendChild(document.createTextNode('play album'));
  6816. const aGoto = div2.appendChild(document.createElement('a'));
  6817. aGoto.style = aStyle;
  6818. aGoto.href = '#';
  6819. aGoto.addEventListener('click', goTo);
  6820. aGoto.appendChild(document.createTextNode('🔝 scroll to album'));
  6821. const aOpen = div2.appendChild(document.createElement('a'));
  6822. aOpen.style = aStyle;
  6823. aOpen.href = '#';
  6824. aOpen.addEventListener('click', open);
  6825. aOpen.appendChild(document.createTextNode('📂 open album'));
  6826. const aNext = div2.appendChild(document.createElement('a'));
  6827. aNext.style = aStyle;
  6828. aNext.href = '#';
  6829. aNext.addEventListener('click', next);
  6830. aNext.appendChild(document.createTextNode('⏭️ next'));
  6831. const aWish = div2.appendChild(document.createElement('a'));
  6832. aWish.style = aStyle;
  6833. aWish.href = '#';
  6834. aWish.addEventListener('click', wishList);
  6835. aWish.appendChild(document.createTextNode('🤍 wishlist'));
  6836. div.appendChild(audio);
  6837. audio.style = `
  6838. width: 100%;
  6839. height: 40px;
  6840. display: block;
  6841. opacity: ${currentStyle.audio_opacity};
  6842. background-color:${currentStyle.audio_bg}`;
  6843. div.style = `
  6844. width: 20%;
  6845. min-width: 200px;
  6846. height: 75px;
  6847. position: fixed;
  6848. right: 0px;
  6849. bottom: 0px;
  6850. display: block;
  6851. border:${currentStyle.div_border};
  6852. border-radius: 5px;
  6853. background-color:${currentStyle.div_bg}`;
  6854. div2.style = `
  6855. text-align: center;`;
  6856. };
  6857. const audio = document.querySelector('body>audio');
  6858. if (audio) {
  6859. audio.addEventListener('timeupdate', makeAudioVisible);
  6860. }
  6861. }
  6862. let feedCurrentItem = null;
  6863. function feedEnablePlayNextItem() {
  6864. // Play next item in feed when current item ends
  6865. const onItemStart = function () {
  6866. // Save item that is currently playing (play button is showing Pause-symbol)
  6867. sleep(2000).then(() => {
  6868. feedCurrentItem = feedCurrentItem || document.querySelector('.story-list .collection-item-container.playing');
  6869. });
  6870. };
  6871. const onItemEnded = function () {
  6872. feedPlayNextItem();
  6873. };
  6874. const audio = document.querySelector('body>audio');
  6875. if (audio) {
  6876. audio.addEventListener('play', onItemStart);
  6877. audio.addEventListener('ended', onItemEnded);
  6878. }
  6879. }
  6880. function feedPlayNextItem() {
  6881. if (feedCurrentItem) {
  6882. // Find next item and click play button
  6883. let isNext = false;
  6884. for (const item of document.querySelectorAll('.story-list .collection-item-container')) {
  6885. if (isNext && item.querySelector('.play-button')) {
  6886. item.querySelector('.play-button').click();
  6887. feedCurrentItem = null;
  6888. return true;
  6889. } else if (item === feedCurrentItem) {
  6890. isNext = true;
  6891. }
  6892. }
  6893. }
  6894. return false;
  6895. }
  6896. function feedAddDiscographyPlayerButtons() {
  6897. const play = function (ev) {
  6898. ev.preventDefault();
  6899. playAlbumFromUrl(this.dataset.url);
  6900. };
  6901. document.querySelectorAll('.collect-item ul').forEach(ul => {
  6902. if (ul.querySelector('li.discographyplayerbutton') || !ul.querySelector('li.buy-now')) {
  6903. return;
  6904. }
  6905. const li = ul.appendChild(ul.querySelector('li.buy-now').cloneNode(true));
  6906. li.classList.remove('buy-now');
  6907. li.classList.add('discographyplayerbutton');
  6908. const a = li.querySelector('a');
  6909. a.dataset.url = a.href;
  6910. a.href = '#';
  6911. a.textContent = 'play album';
  6912. a.addEventListener('click', play);
  6913. const img = li.insertBefore(document.createElement('img'), li.querySelector('a'));
  6914. img.src = 'https://raw.githubusercontent.com/cvzi/Bandcamp-script-deluxe-edition/master/images/icon.png';
  6915. img.style = 'width: 14px; vertical-align: sub;padding:0px 3px 0px 0px;';
  6916. img.alt = 'Play in discography player';
  6917. });
  6918. window.setTimeout(feedAddDiscographyPlayerButtons, 10000);
  6919. }
  6920. function darkMode() {
  6921. // CSS taken from https://userstyles.org/styles/171538/bandcamp-in-dark by Simonus (Version from January 24, 2020)
  6922. // https://userstyles.org/api/v1/styles/css/171538
  6923.  
  6924. let propOpenWrapperBackgroundColor = '#2626268f';
  6925. try {
  6926. const brightnessStr = window.localStorage.getItem('bcsde_bgimage_brightness');
  6927. if (brightnessStr !== null && brightnessStr !== 'null') {
  6928. const brightness = parseFloat(brightnessStr);
  6929. const alpha = (brightness - 50) / 255;
  6930. propOpenWrapperBackgroundColor = `rgba(0, 0, 0, ${alpha})`;
  6931. }
  6932. } catch (e) {
  6933. console.error('Could not access window.localStorage: ' + e);
  6934. }
  6935. addStyle(`
  6936. :root {
  6937. --pgBdColor: #262626;
  6938. --propOpenWrapperBackgroundColor: ${propOpenWrapperBackgroundColor}
  6939. }`);
  6940. addStyle(darkmodeCSS);
  6941. window.setTimeout(humour, 3000);
  6942. darkModeInjected = true;
  6943. }
  6944. async function darkModeOnLoad() {
  6945. const yes = await darkModeMode();
  6946. if (!yes) {
  6947. return;
  6948. }
  6949.  
  6950. // Load body's background image and detect if it is light or dark and adapt it's transparency
  6951. const backgroudImageCSS = window.getComputedStyle(document.body).backgroundImage;
  6952. let imageURL = backgroudImageCSS.match(/["'](.*)["']/);
  6953. let shouldUpdate = false;
  6954. let hasBackgroundImage = false;
  6955. if (imageURL && imageURL[1]) {
  6956. imageURL = imageURL[1];
  6957. shouldUpdate = true;
  6958. hasBackgroundImage = true;
  6959. try {
  6960. const editTime = parseInt(window.localStorage.getItem('bcsde_bgimage_brightness_time'));
  6961. if (Date.now() - editTime < 604800000) {
  6962. shouldUpdate = false;
  6963. }
  6964. } catch (e) {
  6965. console.error('Could not read from window.localStorage: ' + e);
  6966. }
  6967. }
  6968. if (shouldUpdate) {
  6969. const canvas = await loadCrossSiteImage(imageURL);
  6970. const ctx = canvas.getContext('2d');
  6971. const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  6972. let sum = 0.0;
  6973. let div = 0;
  6974. const stepSize = canvas.width * canvas.height / 1000;
  6975. const len = data.length - 4;
  6976. for (let i = 0; i < len; i += 4 * parseInt(stepSize * Math.random())) {
  6977. const v = Math.max(Math.max(data[i], data[i + 1]), data[i + 2]);
  6978. sum += v;
  6979. div++;
  6980. }
  6981. const brightness = sum / div;
  6982. const alpha = (brightness - 50) / 255;
  6983. document.querySelector('#propOpenWrapper').style.backgroundColor = `rgba(0, 0, 0, ${alpha})`;
  6984. try {
  6985. window.localStorage.setItem('bcsde_bgimage_brightness', brightness);
  6986. window.localStorage.setItem('bcsde_bgimage_brightness_time', Date.now());
  6987. } catch (e) {
  6988. console.error('Could not write to window.localStorage: ' + e);
  6989. }
  6990. }
  6991. if (!hasBackgroundImage) {
  6992. // No background image, check background color
  6993. const color = window.getComputedStyle(document.body).backgroundColor;
  6994. if (color) {
  6995. const m = color.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
  6996. if (m) {
  6997. const [, r, g, b] = m;
  6998. if (r < 70 && g < 70 && b < 70) {
  6999. addStyle(`
  7000. :root {
  7001. --propOpenWrapperBackgroundColor: rgb(${r}, ${g}, ${b})
  7002. }
  7003. `);
  7004. }
  7005. }
  7006. }
  7007. }
  7008. // pgBd background color
  7009. if (document.getElementById('custom-design-rules-style')) {
  7010. const customCss = document.getElementById('custom-design-rules-style').textContent;
  7011. if (customCss.indexOf('#pgBd') !== -1) {
  7012. const pgBdStyle = customCss.split('#pgBd')[1].split('}')[0];
  7013. const m = pgBdStyle.match(/background(-color)?\s*:\s*(.+?)[;\s]/m);
  7014. if (m && m.length > 2 && m[2]) {
  7015. const color = css2rgb(m[2]);
  7016. if (color) {
  7017. const [r, g, b] = color;
  7018. if (r < 70 && g < 70 && b < 70) {
  7019. addStyle(`
  7020. :root {
  7021. --pgBdColor: rgb(${r}, ${g}, ${b});
  7022. }
  7023. `);
  7024. }
  7025. }
  7026. }
  7027. }
  7028. }
  7029. }
  7030. async function updateSuntimes() {
  7031. const value = await GM.getValue('darkmode', '1');
  7032. if (value.startsWith('3#')) {
  7033. const data = JSON.parse(value.substring(2));
  7034. const sunData = suntimes(new Date(), data.latitude, data.longitude);
  7035. const newValue = '3#' + JSON.stringify(Object.assign(data, sunData));
  7036. if (newValue !== value) {
  7037. await GM.setValue('darkmode', newValue);
  7038. }
  7039. }
  7040. }
  7041. function confirmDomain() {
  7042. return new Promise(function confirmDomainPromise(resolve) {
  7043. GM.getValue('domains', '{}').then(function (v) {
  7044. const domains = JSON.parse(v);
  7045. if (document.location.hostname in domains) {
  7046. const isBandcamp = domains[document.location.hostname];
  7047. return resolve(isBandcamp);
  7048. } else {
  7049. window.setTimeout(function () {
  7050. const isBandcamp = window.confirm(`${SCRIPT_NAME}
  7051.  
  7052. This page looks like a bandcamp page, but the URL ${document.location.hostname} is not a bandcamp URL.
  7053.  
  7054. Do you want to run the userscript on this page?
  7055.  
  7056. If this is a malicious website, running the userscript may leak personal data (e.g. played albums) to the website`);
  7057. domains[document.location.hostname] = isBandcamp;
  7058. GM.setValue('domains', JSON.stringify(domains)).then(() => resolve(isBandcamp));
  7059. }, 3000);
  7060. }
  7061. });
  7062. });
  7063. }
  7064. async function setDomain(enabled) {
  7065. const domains = JSON.parse(await GM.getValue('domains', '{}'));
  7066. domains[document.location.hostname] = enabled;
  7067. await GM.setValue('domains', JSON.stringify(domains));
  7068. }
  7069. let darkModeModeCurrent = null;
  7070. async function darkModeMode() {
  7071. if (darkModeModeCurrent != null) {
  7072. return darkModeModeCurrent;
  7073. }
  7074. const value = await GM.getValue('darkmode', '1');
  7075. darkModeModeCurrent = false;
  7076. if (value.startsWith('1')) {
  7077. darkModeModeCurrent = true;
  7078. } else if (value.startsWith('2#')) {
  7079. darkModeModeCurrent = nowInTimeRange(value.substring(2));
  7080. } else if (value.startsWith('3#')) {
  7081. const data = JSON.parse(value.substring(2));
  7082. window.setTimeout(updateSuntimes, Math.random() * 10000);
  7083. darkModeModeCurrent = nowInBetween(new Date(data.sunset), new Date(data.sunrise));
  7084. }
  7085. return darkModeModeCurrent;
  7086. }
  7087. function start() {
  7088. // Load settings and enable darkmode
  7089. return new Promise(function startFct(resolve) {
  7090. GM.getValue('enabledFeatures', false).then(value => getEnabledFeatures(value)).then(function () {
  7091. if (BANDCAMP && allFeatures.darkMode.enabled) {
  7092. darkModeMode().then(function (yes) {
  7093. if (yes) {
  7094. darkMode();
  7095. }
  7096. resolve();
  7097. });
  7098. } else {
  7099. resolve();
  7100. }
  7101. });
  7102. });
  7103. }
  7104. function onLoaded() {
  7105. if (!enabledFeaturesLoaded) {
  7106. GM.getValue('enabledFeatures', false).then(value => getEnabledFeatures(value)).then(function () {
  7107. onLoaded();
  7108. });
  7109. return;
  7110. }
  7111. if (!BANDCAMP && document.querySelector('#legal.horizNav li.view-switcher.desktop a,head>meta[name=generator][content=Bandcamp]')) {
  7112. // Page is a bandcamp page but does not have a bandcamp domain
  7113. confirmDomain().then(function (isBandcamp) {
  7114. BANDCAMP = isBandcamp;
  7115. if (isBandcamp) {
  7116. onLoaded();
  7117. GM.registerMenuCommand(SCRIPT_NAME + ' - disable on this page', () => setDomain(false).then(() => document.location.reload()));
  7118. } else {
  7119. GM.registerMenuCommand(SCRIPT_NAME + ' - enable on this page', () => setDomain(true).then(() => document.location.reload()));
  7120. }
  7121. });
  7122. return;
  7123. } else if (!BANDCAMP && !CAMPEXPLORER) {
  7124. // Not a bandcamp page -> quit
  7125. return;
  7126. }
  7127. const IS_PLAYER_URL = document.location.href.startsWith(PLAYER_URL);
  7128. const IS_PLAYER_FRAME = IS_PLAYER_URL && document.location.search.indexOf('iframe');
  7129. if (allFeatures.darkMode.enabled) {
  7130. // Darkmode in start() is only run on bandcamp domains
  7131. if (!darkModeInjected) {
  7132. darkModeMode().then(function (yes) {
  7133. if (yes) {
  7134. darkMode();
  7135. }
  7136. });
  7137. }
  7138. window.setTimeout(darkModeOnLoad, 0);
  7139. }
  7140. storeTralbumDataPermanentlySwitch = allFeatures.keepLibrary.enabled;
  7141. const maintenanceContent = document.querySelector('.content');
  7142. if (maintenanceContent && maintenanceContent.textContent.indexOf('are offline') !== -1) {
  7143. console.log('Maintenance detected');
  7144. } else {
  7145. if (NOEMOJI) {
  7146. addStyle('@font-face{font-family:Symbola;src:local("Symbola Regular"),local("Symbola"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.woff2) format("woff2"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.woff) format("woff"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.ttf) format("truetype"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.otf) format("opentype"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.svg#Symbola) format("svg")}' + '.sharepanelchecksymbol,.bdp_check_onlinkhover_symbol,.bdp_check_onchecked_symbol,.volumeSymbol,.downloaddisk,.downloadlink,#user-nav .menubar-symbol,.listened-symbol,.mark-listened-symbol,.minimizebutton{font-family:Symbola,Quivira,"Segoe UI Symbol","Segoe UI Emoji",Arial,sans-serif}' + '.downloaddisk,.downloadlink{font-weight: bolder}');
  7147. }
  7148. GM.getValue('notification_timeout', NOTIFICATION_TIMEOUT).then(function (ms) {
  7149. NOTIFICATION_TIMEOUT = parseInt(ms);
  7150. });
  7151. if (allFeatures.releaseReminder.enabled && !IS_PLAYER_FRAME) {
  7152. showPastReleases();
  7153. }
  7154. if (document.querySelector('#indexpage .indexpage_list_cell a[href*="/album/"] img')) {
  7155. // Index pages are almost like discography page. To make them compatible, let's add the class names from the discography page
  7156. document.querySelector('#indexpage').classList.add('music-grid');
  7157. document.querySelectorAll('#indexpage .indexpage_list_cell').forEach(cell => cell.classList.add('music-grid-item'));
  7158. addStyle('#indexpage .ipCellImage { position:relative }');
  7159. }
  7160. if (document.querySelector('.search .result-items .searchresult img')) {
  7161. // Search result pages. To make them compatible, let's add the class names from the discography page
  7162. document.querySelector('.search .result-items').classList.add('music-grid');
  7163. // Add class name to albums, tracks, labels and artists
  7164. document.querySelectorAll(`
  7165. .search .result-items .searchresult[data-search*='"type":"a"'],
  7166. .search .result-items .searchresult[data-search*='"type":"t"'],
  7167. .search .result-items .searchresult[data-search*='"type":"b"']
  7168. `).forEach(cell => cell.classList.add('music-grid-item'));
  7169. }
  7170. if (allFeatures.discographyplayer.enabled && document.querySelector('.music-grid .music-grid-item a[href*="/album/"] img,.music-grid .music-grid-item a[href*="/track/"] img')) {
  7171. // Discography page
  7172. makeAlbumCoversGreat();
  7173. }
  7174. if (document.querySelector('.inline_player')) {
  7175. // Album page with player
  7176. if (allFeatures.thetimehascome.enabled) {
  7177. removeTheTimeHasComeToOpenThyHeartWallet();
  7178. }
  7179. if (allFeatures.albumPageVolumeBar.enabled) {
  7180. window.setTimeout(addVolumeBarToAlbumPage, 3000);
  7181. }
  7182. if (allFeatures.albumPageDownloadLinks.enabled) {
  7183. window.setTimeout(addDownloadLinksToAlbumPage, 500);
  7184. }
  7185. if (allFeatures.albumPageLyrics.enabled) {
  7186. window.setTimeout(addLyricsToAlbumPage, 500);
  7187. }
  7188. if (allFeatures.discographyplayer.enabled) {
  7189. addOpenDiscographyPlayerFromAlbumPage();
  7190. }
  7191. }
  7192. if (document.location.pathname.startsWith('/tag/')) {
  7193. // Tag search page
  7194. if (allFeatures.tagSearchPlayer.enabled) {
  7195. makeTagSearchCoversGreat();
  7196. }
  7197. }
  7198. if (document.querySelector('.share-panel-wrapper-desktop')) {
  7199. // Album page with Share,Embed,Wishlist links
  7200.  
  7201. if (allFeatures.markasplayedEverywhere.enabled) {
  7202. addListenedButtonToCollectControls();
  7203. }
  7204. if (document.location.hash === '#collect-wishlist') {
  7205. clickAddToWishlist();
  7206. }
  7207. if (unsafeWindow.TralbumData && unsafeWindow.TralbumData.current && unsafeWindow.TralbumData.current.release_date) {
  7208. addReleaseDateButton();
  7209. }
  7210. }
  7211. if (unsafeWindow.TralbumData && unsafeWindow.TralbumData.tralbum_collect_info && unsafeWindow.TralbumData.tralbum_collect_info.is_purchased) {
  7212. showDownloadLinkOnAlbumPage();
  7213. }
  7214. GM.registerMenuCommand(SCRIPT_NAME + ' - Settings', mainMenu);
  7215. if (document.getElementById('user-nav')) {
  7216. appendMainMenuButtonTo(document.getElementById('user-nav'));
  7217. } else if (document.getElementById('customHeaderWrapper')) {
  7218. appendMainMenuButtonLeftTo(document.getElementById('customHeaderWrapper'));
  7219. } else if (document.querySelector('#corphome-autocomplete-form ul.hd-nav.corp-nav')) {
  7220. // Homepage and not logged in
  7221. appendMainMenuButtonTo(document.querySelector('#corphome-autocomplete-form ul.hd-nav.corp-nav'));
  7222. }
  7223. if (document.querySelector('.hd-banner-2018')) {
  7224. // Move the "we are hiring" banner (not loggin in)
  7225. document.querySelector('.hd-banner-2018').style.left = '-500px';
  7226. }
  7227. if (document.querySelector('.li-banner-2018')) {
  7228. // Remove the "we are hiring" banner (logged in)
  7229. document.querySelector('.li-banner-2018').remove();
  7230. }
  7231. if (document.getElementById('carousel-player') || document.querySelector('.play-carousel')) {
  7232. window.setTimeout(makeCarouselPlayerGreatAgain, 5000);
  7233. }
  7234. if (document.querySelector('ol#grid-tabs li') && document.querySelector('.fan-bio-pic-upload-container')) {
  7235. const listenedTabLink = makeListenedListTabLink();
  7236. if (document.location.hash === '#listened-tab') {
  7237. window.setTimeout(function resetGridTabs() {
  7238. document.querySelector('#grid-tabs .active').classList.remove('active');
  7239. document.querySelector('#grids .grid.active').classList.remove('active');
  7240. listenedTabLink.classList.add('active');
  7241. listenedTabLink.click();
  7242. }, 500);
  7243. }
  7244. }
  7245. if (allFeatures.albumPageVolumeBar.enabled) {
  7246. restoreVolume();
  7247. }
  7248. if (allFeatures.markasplayedEverywhere.enabled) {
  7249. makeAlbumLinksGreat();
  7250. }
  7251. if (allFeatures.backupReminder.enabled && !IS_PLAYER_FRAME) {
  7252. checkBackupStatus();
  7253. }
  7254. if (allFeatures.customReleaseDateFormat.enabled) {
  7255. formatReleaseDateOnAlbumPage();
  7256. }
  7257. if (allFeatures.showAlbumID.enabled) {
  7258. showAlbumID();
  7259. }
  7260. if (allFeatures.feedShowOnlyNewReleases.enabled && document.querySelector('#stories li.story')) {
  7261. feedShowOnlyNewReleases();
  7262. }
  7263. if (allFeatures.feedShowAudioControls.enabled && document.querySelector('#stories li.story')) {
  7264. feedAddAudioControls();
  7265. }
  7266. feedEnablePlayNextItem();
  7267. feedAddDiscographyPlayerButtons();
  7268. if (CAMPEXPLORER) {
  7269. let lastTagsText = document.querySelector('.tags') ? document.querySelector('.tags').textContent : '';
  7270. window.setInterval(function () {
  7271. const tagsText = document.querySelector('.tags') ? document.querySelector('.tags').textContent : '';
  7272. if (lastTagsText !== tagsText) {
  7273. lastTagsText = tagsText;
  7274. if (allFeatures.discographyplayer.enabled) {
  7275. makeAlbumCoversGreat();
  7276. }
  7277. if (allFeatures.markasplayedEverywhere.enabled) {
  7278. makeAlbumLinksGreat();
  7279. }
  7280. }
  7281. }, 3000);
  7282.  
  7283. // Add a little space at the bottom of the page to accommodate the discographyplayer at the bottom
  7284. document.body.style.paddingBottom = '200px';
  7285. // Move the sidebar to the left
  7286. document.querySelectorAll('.sidebar').forEach(function (div) {
  7287. div.style.alignSelf = 'flex-start';
  7288. div.querySelectorAll('.shortcuts').forEach(function (shortcuts) {
  7289. shortcuts.style.borderRadius = '0 1em 1em 0';
  7290. });
  7291. });
  7292. }
  7293. if (IS_PLAYER_URL) {
  7294. showExplorer();
  7295. } else if (document.location.pathname === LYRICS_EMPTY_PATH) {
  7296. initGenius();
  7297. }
  7298. GM.getValue('musicPlayerState', '{}').then(function restoreState(s) {
  7299. if (s !== '{}') {
  7300. GM.setValue('musicPlayerState', '{}');
  7301. musicPlayerRestoreState(JSON.parse(s));
  7302. }
  7303. });
  7304. if (document.querySelector('.inline_player') && unsafeWindow.TralbumData && unsafeWindow.TralbumData.current && unsafeWindow.TralbumData.trackinfo) {
  7305. const TralbumData = correctTralbumData(JSON.parse(JSON.stringify(unsafeWindow.TralbumData)), document.body.innerHTML);
  7306. storeTralbumDataPermanently(TralbumData);
  7307. }
  7308. }
  7309. }
  7310. start().then(function () {
  7311. if (document.readyState === 'loading') {
  7312. document.addEventListener('DOMContentLoaded', onLoaded);
  7313. } else {
  7314. onLoaded();
  7315. }
  7316. });
  7317.  
  7318. })(React, ReactDOM);