-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsoundcloud-restore-playback.user.js
124 lines (115 loc) · 3.57 KB
/
soundcloud-restore-playback.user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// ==UserScript==
// @name SoundCloud Restore Playback
// @description Saves/restores playback position on SoundCloud (only for tracks longer than 5 minutes)
// @namespace https://github.com/crabvk
// @version 0.4.1
// @author Vyacheslav Konovalov
// @match https://soundcloud.com/*
// @license MIT
// @noframes
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @downloadURL https://raw.githubusercontent.com/crabvk/userscripts/master/soundcloud-restore-playback.user.js
// @homepageURL https://github.com/crabvk/userscripts
// ==/UserScript==
const getKey = (player) =>
player.querySelector('.playbackSoundBadge__titleLink').getAttribute('href')
const getTimeline = (player) => player.querySelector('.playbackTimeline__progressWrapper')
/**
* Clicks on the timeline according to factor.
*
* @param {number} timeline The timeline wrapper element on which to click.
* @param {number} factor Current position divided by track duration.
*/
function clickTimeline(timeline, factor) {
const rect = timeline.getBoundingClientRect()
const args = {
view: unsafeWindow,
bubbles: true,
clientX: rect.x + Math.floor(rect.width * factor),
clientY: rect.y + 10,
}
timeline.dispatchEvent(new MouseEvent('mousedown', args))
timeline.dispatchEvent(new MouseEvent('mouseup', args))
}
/**
* Restores timeline position.
*/
function restorePlayback(player) {
const timeline = getTimeline(player)
const duration = Number(timeline.getAttribute('aria-valuemax'))
// Skip tracks shorter than 5 minutes.
if (duration < 300) {
return
}
const key = getKey(player)
const position = GM_getValue(key) || 0
if (position > 0) {
// Do not restore position from the last 30 seconds of the track.
if (position < duration - 30) {
clickTimeline(timeline, position / duration)
} else {
GM_deleteValue(key)
}
}
return [key, position]
}
function observePlayback(player) {
let lastKey
let lastPosition = -42
let isRestore = false
new MutationObserver((mutations) => {
const mutation = mutations.findLast(
(m) => m.type === 'attributes' && m.attributeName === 'aria-valuenow'
)
if (mutation === undefined) {
return
}
const duration = Number(mutation.target.getAttribute('aria-valuemax'))
// Skip tracks shorter than 5 minutes.
if (duration < 300) {
return
}
let key = getKey(player)
if (lastKey === undefined) {
lastKey = key
}
let position
// Listening to the same track.
if (lastKey === key) {
if (isRestore) {
isRestore = false
return
}
position = Number(mutation.target.getAttribute('aria-valuenow'))
if (
// For each 5 seconds of playback,
(position > 0 && position % 5 === 0) ||
// or user changed the position.
Math.abs(lastPosition - position) > 4
) {
GM_setValue(key, position)
}
}
// User changed the track.
else {
// prettier-ignore
isRestore = true
[key, position] = restorePlayback(player)
GM_setValue(key, position)
}
lastKey = key
lastPosition = position
}).observe(getTimeline(player), { attributes: true })
}
// Waiting for the first bunch of mutations on the player element.
const player = document.body.querySelector('#app .playControls')
new MutationObserver((_mutations, observer) => {
observer.disconnect()
setTimeout(() => {
restorePlayback(player)
observePlayback(player)
}, 1000)
}).observe(player, { subtree: true, childList: true })