Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
SpotifyBackup
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Model registry
Operate
Environments
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Robert Goldmann
SpotifyBackup
Compare revisions
4c77e89ecd076e4cac38e56654ea06d9edb779a1 to 7effe35e622b2ec10633f1e32418efa2022a62df
Compare revisions
Changes are shown as if the
source
revision was being merged into the
target
revision.
Learn more about comparing revisions.
Source
deadlocker8/SpotifyBackup
Select target project
No results found
7effe35e622b2ec10633f1e32418efa2022a62df
Select Git revision
Branches
master
Tags
v1.0.0
v1.1.0
v1.10.0
v1.11.0
v1.12.0
v1.12.1
v1.12.2
8 results
Swap
Target
deadlocker8/SpotifyBackup
Select target project
deadlocker8/SpotifyBackup
1 result
4c77e89ecd076e4cac38e56654ea06d9edb779a1
Select Git revision
Branches
master
Tags
v1.0.0
v1.1.0
v1.10.0
v1.11.0
v1.12.0
v1.12.1
v1.12.2
8 results
Show changes
Only incoming changes from source
Include changes to target since source was created
Compare
Commits on Source (5)
SpotifyRecorder: improved final duration check
· 9b062917
Robert Goldmann
authored
1 year ago
9b062917
SpotifyRecorder: added settings to specify start index and limit
· aeb63f39
Robert Goldmann
authored
1 year ago
aeb63f39
SpotifyRecorder: handle unplayable songs
· 0fe7879b
Robert Goldmann
authored
1 year ago
0fe7879b
SpotifyRecorder: print summary of skipped and error track numbers
· 44571bdb
Robert Goldmann
authored
1 year ago
44571bdb
SpotifyRecorder: make sure no track is playing when starting to record next track
· 7effe35e
Robert Goldmann
authored
1 year ago
7effe35e
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
SpotifyRecorder.py
+73
-36
73 additions, 36 deletions
SpotifyRecorder.py
config/settings-recorder-example.json
+2
-0
2 additions, 0 deletions
config/settings-recorder-example.json
with
75 additions
and
36 deletions
SpotifyRecorder.py
View file @
7effe35e
...
...
@@ -26,7 +26,9 @@ class SpotifyRecorder:
playlist
:
Dict
[
str
,
str
],
spotifyDeviceName
:
str
,
audioDeviceName
:
str
,
destinationFolder
:
str
):
destinationFolder
:
str
,
startNumber
:
int
=
0
,
limit
:
int
=
-
1
):
self
.
_clientID
=
clientID
self
.
_clientSecret
=
clientSecret
self
.
_redirectUrl
=
redirectUrl
...
...
@@ -36,11 +38,11 @@ class SpotifyRecorder:
self
.
_spotifyDeviceName
=
spotifyDeviceName
self
.
_audioDeviceName
=
audioDeviceName
self
.
_destinationFolder
=
destinationFolder
self
.
_startNumber
=
startNumber
self
.
_limit
=
limit
os
.
makedirs
(
self
.
_destinationFolder
,
exist_ok
=
True
)
# TODO: options specify range (start / stop track) or only offset index + limit
self
.
_spotify
=
self
.
login
()
def
login
(
self
)
->
spotipy
.
Spotify
:
...
...
@@ -53,16 +55,14 @@ class SpotifyRecorder:
return
spotipy
.
Spotify
(
client_credentials_manager
=
client_credentials_manager
)
def
run
(
self
):
LOGGER
.
info
(
f
'
>>> Fetching all tracks for playlist
{
self
.
_playlist
[
"
name
"
]
}
...
'
)
allTracks
=
[]
LOGGER
.
info
(
f
'
Fetching all tracks for playlist
{
self
.
_playlist
[
"
name
"
]
}
...
'
)
playlist
=
self
.
__get_playlist
(
self
.
_playlist
[
'
user
'
],
self
.
_playlist
[
'
id
'
])
allTracks
.
extend
(
self
.
__get_tracks
(
playlist
))
LOGGER
.
info
(
f
'
>>> Found
{
len
(
allTracks
)
}
tracks
'
)
tracks
=
self
.
__get_tracks
(
playlist
)
self
.
__record_tracks
(
allT
racks
)
self
.
__record_tracks
(
t
racks
)
def
__get_playlist
(
self
,
username
:
str
,
playlistID
:
str
)
->
Dict
:
LOGGER
.
info
(
f
'
>>>
Fetching playlist with ID:
{
playlistID
}
by
{
username
}
...
'
)
LOGGER
.
info
(
f
'
Fetching playlist with ID:
{
playlistID
}
by
{
username
}
...
'
)
identifier
=
f
'
spotify:user:
{
username
}
:playlist:
{
playlistID
}
'
playlist
=
self
.
_spotify
.
playlist
(
identifier
)
LOGGER
.
info
(
f
'
Found playlist
"
{
playlist
[
"
name
"
]
}
"'
)
...
...
@@ -76,7 +76,7 @@ class SpotifyRecorder:
tracks
=
self
.
_spotify
.
next
(
tracks
)
results
.
extend
(
tracks
[
'
items
'
])
LOGGER
.
info
(
f
'
F
etche
d
{
len
(
result
s
)
}
tracks
'
)
LOGGER
.
info
(
f
'
F
oun
d
{
len
(
track
s
)
}
tracks
in playlist
'
)
return
results
def
__extract_track_uris
(
self
,
tracks
:
List
)
->
List
[
str
]:
...
...
@@ -85,34 +85,56 @@ class SpotifyRecorder:
def
__record_tracks
(
self
,
tracks
:
list
):
deviceId
=
self
.
__get_device_id_by_name
(
self
.
_spotifyDeviceName
)
recordedTracks
=
[]
skippedTracks
=
[]
errorTracks
=
[]
for
index
,
track
in
enumerate
(
tracks
[:
2
]):
recordedTrackNumbers
=
[]
skippedTrackNumbers
=
[]
errorTrackNumbers
=
[]
if
self
.
_limit
==
-
1
:
LOGGER
.
info
(
f
'
Recording track #
{
self
.
_startNumber
}
to all
'
)
tracks
=
tracks
[
self
.
_startNumber
-
1
:]
else
:
LOGGER
.
info
(
f
'
Recording track #
{
self
.
_startNumber
}
to (including) #
{
self
.
_startNumber
+
self
.
_limit
-
1
}
'
)
tracks
=
tracks
[
self
.
_startNumber
-
1
:
self
.
_startNumber
-
1
+
self
.
_limit
]
for
index
,
track
in
enumerate
(
tracks
):
indexInPlaylist
=
self
.
_startNumber
+
index
if
track
[
'
is_local
'
]:
# TODO:
# It's not possible to add a local track to a playlist using the web API.
# https://github.com/plamere/spotipy/issues/793#issuecomment-1082421408
LOGGER
.
info
(
f
'
Skipping local track
"
{
track
[
"
track
"
][
"
name
"
]
}
"'
)
skippedTracks
.
append
(
track
[
'
track
'
][
'
name
'
]
)
skippedTrack
Number
s
.
append
(
indexInPlaylist
)
continue
LOGGER
.
info
(
f
'
Recording track
{
index
+
1
}
/
{
len
(
tracks
)
}
:
"
{
track
[
"
track
"
][
"
name
"
]
}
"
...
'
)
LOGGER
.
info
(
f
'
>>> Recording track
{
index
+
1
}
/
{
len
(
tracks
)
}
: #
{
indexInPlaylist
}
"
{
track
[
"
track
"
][
"
name
"
]
}
"
...
'
)
try
:
filePath
=
self
.
__determine_file_path
(
index
+
1
,
track
)
self
.
__stop_playback_if_playing
(
deviceId
)
filePath
=
self
.
__determine_file_path
(
indexInPlaylist
+
1
,
track
)
recorder
=
SpotifyAudioRecorder
(
self
.
_audioDeviceName
,
filePath
)
with
recorder
.
record
():
self
.
__play_track
(
deviceId
,
track
[
'
track
'
][
'
uri
'
])
self
.
__wait_for_track_playing
()
self
.
__wait_for_track_end
(
track
)
timeWaitedForPlaying
=
self
.
__wait_for_track_playing
(
track
[
'
track
'
][
'
id
'
]
)
self
.
__wait_for_track_end
(
track
,
timeWaitedForPlaying
)
recordedTracks
.
append
(
track
[
'
track
'
][
'
name
'
]
)
recordedTrack
Number
s
.
append
(
indexInPlaylist
)
except
Exception
as
e
:
LOGGER
.
error
(
f
'
An error occurred while recording track
"
{
track
[
"
track
"
][
"
name
"
]
}
"'
,
exc_info
=
e
)
errorTracks
.
append
(
track
[
'
track
'
][
'
name
'
]
)
errorTrack
Number
s
.
append
(
indexInPlaylist
)
LOGGER
.
info
(
'
### DONE ###
'
)
LOGGER
.
info
(
f
'
{
len
(
tracks
)
}
tracks,
{
len
(
recordedTracks
)
}
recorded,
{
len
(
skippedTracks
)
}
skipped,
{
len
(
errorTracks
)
}
errors
'
)
LOGGER
.
info
(
'
>>> Skipped <<<
'
)
for
number
in
skippedTrackNumbers
:
LOGGER
.
info
(
f
'
Skipped #
{
number
}
'
)
LOGGER
.
info
(
'
>>> Errors <<<
'
)
for
number
in
errorTrackNumbers
:
LOGGER
.
info
(
f
'
Error #
{
number
}
'
)
LOGGER
.
info
(
f
'
###
{
len
(
tracks
)
}
tracks,
{
len
(
recordedTrackNumbers
)
}
recorded,
{
len
(
skippedTrackNumbers
)
}
skipped,
{
len
(
errorTrackNumbers
)
}
errors ###
'
)
def
__determine_file_path
(
self
,
index
:
int
,
track
)
->
str
:
artists
=
track
[
'
track
'
][
'
artists
'
]
...
...
@@ -120,10 +142,10 @@ class SpotifyRecorder:
fileName
=
f
'
{
index
}
-
{
artists
}
-
{
track
[
"
track
"
][
"
name
"
]
}
.wav
'
return
os
.
path
.
join
(
self
.
_destinationFolder
,
fileName
)
def
__wait_for_track_end
(
self
,
track
)
:
def
__wait_for_track_end
(
self
,
track
,
timeWaitedForPlaying
:
float
)
->
None
:
trackDurationInMs
=
track
[
'
track
'
][
'
duration_ms
'
]
trackDurationInSeconds
=
trackDurationInMs
//
1000
LOGGER
.
info
(
f
'
Track duration:
{
self
.
__convert_seconds_to_duration
(
trackDurationInSeconds
)
}
'
)
LOGGER
.
info
(
f
'
\t
Track duration:
{
self
.
__convert_seconds_to_duration
(
trackDurationInSeconds
)
}
'
)
startTime
=
time
.
time
()
...
...
@@ -138,21 +160,22 @@ class SpotifyRecorder:
if
remainingTimeInMs
>
self
.
_THRESHOLD_TRACK_END_IN_MS
:
sleepTime
=
remainingTimeInSeconds
/
2
LOGGER
.
debug
(
f
'
Waiting for track to end (remaining:
'
LOGGER
.
debug
(
f
'
\t\t
Waiting for track to end (remaining:
'
f
'
{
self
.
__convert_seconds_to_duration
(
remainingTimeInSeconds
)
}
,
'
f
'
sleep:
{
self
.
__convert_seconds_to_duration
(
sleepTime
)
}
s)...
'
)
f
'
sleep:
{
self
.
__convert_seconds_to_duration
(
int
(
sleepTime
)
)
}
s)...
'
)
time
.
sleep
(
sleepTime
)
else
:
waitDuration
=
int
(
time
.
time
()
-
startTime
)
waitDuration
+=
timeWaitedForPlaying
if
waitDuration
<
trackDurationInSeconds
:
if
waitDuration
<
trackDurationInSeconds
-
self
.
_WAIT_TIME_TRACK_PLAYING_SHORT_IN_S
:
raise
RuntimeError
(
f
'
Track finished too early (waited:
{
waitDuration
}
s, expected:
{
trackDurationInSeconds
}
s)
'
)
if
waitDuration
>
trackDurationInSeconds
+
self
.
_WAIT_TIME_TRACK_PLAYING_SHORT_IN_S
*
3
:
raise
RuntimeError
(
f
'
Track finished too late (waited:
{
waitDuration
}
s, expected:
{
trackDurationInSeconds
}
)
'
)
LOGGER
.
debug
(
f
'
Track finished. Waited
{
waitDuration
}
s, expected
{
trackDurationInSeconds
}
s, OK
'
)
LOGGER
.
info
(
f
'
Track finished. Waited
{
waitDuration
}
s, expected
{
trackDurationInSeconds
}
s, OK
'
)
break
@staticmethod
...
...
@@ -170,19 +193,31 @@ class SpotifyRecorder:
raise
RuntimeError
(
'
Device not found
'
)
def
__play_track
(
self
,
deviceId
:
str
,
trackUri
:
str
):
def
__play_track
(
self
,
deviceId
:
str
,
trackUri
:
str
)
->
None
:
self
.
_spotify
.
start_playback
(
device_id
=
deviceId
,
uris
=
[
trackUri
])
def
__wait_for_track_playing
(
self
)
->
None
:
LOGGER
.
debug
(
f
'
Wait for track to start playing...
'
)
def
__stop_playback_if_playing
(
self
,
deviceId
:
str
)
->
None
:
if
self
.
_spotify
.
current_playback
()[
'
is_playing
'
]:
self
.
_spotify
.
pause_playback
(
device_id
=
deviceId
)
def
__wait_for_track_playing
(
self
,
expectedTrackId
:
str
)
->
float
:
LOGGER
.
debug
(
f
'
\t\t
Wait for track to start playing...
'
)
startTime
=
time
.
time
()
duration
=
0
while
time
.
time
()
-
startTime
<
self
.
_MAX_WAIT_TIME_TRACK_STARTING_IN_S
:
if
self
.
_spotify
.
current_playback
()[
'
is_playing
'
]:
LOGGER
.
debug
(
f
'
Track started playing after
{
time
.
time
()
-
startTime
:
.
1
f
}
s
'
)
currentPlayback
=
self
.
_spotify
.
current_playback
()
if
currentPlayback
[
'
is_playing
'
]:
duration
=
time
.
time
()
-
startTime
if
currentPlayback
[
'
item
'
][
'
id
'
]
==
expectedTrackId
:
LOGGER
.
debug
(
f
'
\t\t
Track started playing after
{
duration
:
.
1
f
}
s
'
)
break
else
:
raise
RuntimeError
(
f
'
Wrong track started playing (actual:
{
currentPlayback
[
"
item
"
][
"
id
"
]
}
, expected:
{
expectedTrackId
}
)
'
)
time
.
sleep
(
1
)
return
duration
if
__name__
==
'
__main__
'
:
with
open
(
'
config/settings-recorder.json
'
,
'
r
'
,
encoding
=
'
utf-8
'
)
as
f
:
...
...
@@ -196,7 +231,9 @@ if __name__ == '__main__':
SETTINGS
[
'
playlist
'
],
SETTINGS
[
'
spotifyDeviceName
'
],
SETTINGS
[
'
audioDeviceName
'
],
SETTINGS
[
'
destinationFolder
'
])
SETTINGS
[
'
destinationFolder
'
],
SETTINGS
[
'
startNumber
'
],
SETTINGS
[
'
limit
'
],
)
spotifyBackup
.
run
()
...
...
This diff is collapsed.
Click to expand it.
config/settings-recorder-example.json
View file @
7effe35e
...
...
@@ -10,6 +10,8 @@
"spotifyDeviceName"
:
"MYDEVICE"
,
"audioDeviceName"
:
"3/4 - Musik (2- GIGAPort HD Audio driver) [Loopback]"
,
"destinationFolder"
:
""
,
"startNumber"
:
1
,
"limit"
:
-1
,
"redirectUrl"
:
"http://localhost:8080"
,
"openBrowser"
:
false
,
"cacheFilePath"
:
".cache"
...
...
This diff is collapsed.
Click to expand it.