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
Loading items
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
Jan 13, 2024
9b062917
SpotifyRecorder: added settings to specify start index and limit
· aeb63f39
Robert Goldmann
authored
Jan 13, 2024
aeb63f39
SpotifyRecorder: handle unplayable songs
· 0fe7879b
Robert Goldmann
authored
Jan 13, 2024
0fe7879b
SpotifyRecorder: print summary of skipped and error track numbers
· 44571bdb
Robert Goldmann
authored
Jan 13, 2024
44571bdb
SpotifyRecorder: make sure no track is playing when starting to record next track
· 7effe35e
Robert Goldmann
authored
Jan 13, 2024
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:
...
@@ -26,7 +26,9 @@ class SpotifyRecorder:
playlist
:
Dict
[
str
,
str
],
playlist
:
Dict
[
str
,
str
],
spotifyDeviceName
:
str
,
spotifyDeviceName
:
str
,
audioDeviceName
:
str
,
audioDeviceName
:
str
,
destinationFolder
:
str
):
destinationFolder
:
str
,
startNumber
:
int
=
0
,
limit
:
int
=
-
1
):
self
.
_clientID
=
clientID
self
.
_clientID
=
clientID
self
.
_clientSecret
=
clientSecret
self
.
_clientSecret
=
clientSecret
self
.
_redirectUrl
=
redirectUrl
self
.
_redirectUrl
=
redirectUrl
...
@@ -36,11 +38,11 @@ class SpotifyRecorder:
...
@@ -36,11 +38,11 @@ class SpotifyRecorder:
self
.
_spotifyDeviceName
=
spotifyDeviceName
self
.
_spotifyDeviceName
=
spotifyDeviceName
self
.
_audioDeviceName
=
audioDeviceName
self
.
_audioDeviceName
=
audioDeviceName
self
.
_destinationFolder
=
destinationFolder
self
.
_destinationFolder
=
destinationFolder
self
.
_startNumber
=
startNumber
self
.
_limit
=
limit
os
.
makedirs
(
self
.
_destinationFolder
,
exist_ok
=
True
)
os
.
makedirs
(
self
.
_destinationFolder
,
exist_ok
=
True
)
# TODO: options specify range (start / stop track) or only offset index + limit
self
.
_spotify
=
self
.
login
()
self
.
_spotify
=
self
.
login
()
def
login
(
self
)
->
spotipy
.
Spotify
:
def
login
(
self
)
->
spotipy
.
Spotify
:
...
@@ -53,16 +55,14 @@ class SpotifyRecorder:
...
@@ -53,16 +55,14 @@ class SpotifyRecorder:
return
spotipy
.
Spotify
(
client_credentials_manager
=
client_credentials_manager
)
return
spotipy
.
Spotify
(
client_credentials_manager
=
client_credentials_manager
)
def
run
(
self
):
def
run
(
self
):
LOGGER
.
info
(
f
'
>>> Fetching all tracks for playlist
{
self
.
_playlist
[
"
name
"
]
}
...
'
)
LOGGER
.
info
(
f
'
Fetching all tracks for playlist
{
self
.
_playlist
[
"
name
"
]
}
...
'
)
allTracks
=
[]
playlist
=
self
.
__get_playlist
(
self
.
_playlist
[
'
user
'
],
self
.
_playlist
[
'
id
'
])
playlist
=
self
.
__get_playlist
(
self
.
_playlist
[
'
user
'
],
self
.
_playlist
[
'
id
'
])
allTracks
.
extend
(
self
.
__get_tracks
(
playlist
))
tracks
=
self
.
__get_tracks
(
playlist
)
LOGGER
.
info
(
f
'
>>> Found
{
len
(
allTracks
)
}
tracks
'
)
self
.
__record_tracks
(
allT
racks
)
self
.
__record_tracks
(
t
racks
)
def
__get_playlist
(
self
,
username
:
str
,
playlistID
:
str
)
->
Dict
:
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
}
'
identifier
=
f
'
spotify:user:
{
username
}
:playlist:
{
playlistID
}
'
playlist
=
self
.
_spotify
.
playlist
(
identifier
)
playlist
=
self
.
_spotify
.
playlist
(
identifier
)
LOGGER
.
info
(
f
'
Found playlist
"
{
playlist
[
"
name
"
]
}
"'
)
LOGGER
.
info
(
f
'
Found playlist
"
{
playlist
[
"
name
"
]
}
"'
)
...
@@ -76,7 +76,7 @@ class SpotifyRecorder:
...
@@ -76,7 +76,7 @@ class SpotifyRecorder:
tracks
=
self
.
_spotify
.
next
(
tracks
)
tracks
=
self
.
_spotify
.
next
(
tracks
)
results
.
extend
(
tracks
[
'
items
'
])
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
return
results
def
__extract_track_uris
(
self
,
tracks
:
List
)
->
List
[
str
]:
def
__extract_track_uris
(
self
,
tracks
:
List
)
->
List
[
str
]:
...
@@ -85,34 +85,56 @@ class SpotifyRecorder:
...
@@ -85,34 +85,56 @@ class SpotifyRecorder:
def
__record_tracks
(
self
,
tracks
:
list
):
def
__record_tracks
(
self
,
tracks
:
list
):
deviceId
=
self
.
__get_device_id_by_name
(
self
.
_spotifyDeviceName
)
deviceId
=
self
.
__get_device_id_by_name
(
self
.
_spotifyDeviceName
)
recordedTracks
=
[]
recordedTrackNumbers
=
[]
skippedTracks
=
[]
skippedTrackNumbers
=
[]
errorTracks
=
[]
errorTrackNumbers
=
[]
for
index
,
track
in
enumerate
(
tracks
[:
2
]):
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
'
]:
if
track
[
'
is_local
'
]:
# TODO:
# It's not possible to add a local track to a playlist using the web API.
# 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
# https://github.com/plamere/spotipy/issues/793#issuecomment-1082421408
LOGGER
.
info
(
f
'
Skipping local track
"
{
track
[
"
track
"
][
"
name
"
]
}
"'
)
LOGGER
.
info
(
f
'
Skipping local track
"
{
track
[
"
track
"
][
"
name
"
]
}
"'
)
skippedTracks
.
append
(
track
[
'
track
'
][
'
name
'
]
)
skippedTrack
Number
s
.
append
(
indexInPlaylist
)
continue
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
:
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
)
recorder
=
SpotifyAudioRecorder
(
self
.
_audioDeviceName
,
filePath
)
with
recorder
.
record
():
with
recorder
.
record
():
self
.
__play_track
(
deviceId
,
track
[
'
track
'
][
'
uri
'
])
self
.
__play_track
(
deviceId
,
track
[
'
track
'
][
'
uri
'
])
self
.
__wait_for_track_playing
()
timeWaitedForPlaying
=
self
.
__wait_for_track_playing
(
track
[
'
track
'
][
'
id
'
]
)
self
.
__wait_for_track_end
(
track
)
self
.
__wait_for_track_end
(
track
,
timeWaitedForPlaying
)
recordedTracks
.
append
(
track
[
'
track
'
][
'
name
'
]
)
recordedTrack
Number
s
.
append
(
indexInPlaylist
)
except
Exception
as
e
:
except
Exception
as
e
:
LOGGER
.
error
(
f
'
An error occurred while recording track
"
{
track
[
"
track
"
][
"
name
"
]
}
"'
,
exc_info
=
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
(
'
### 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
:
def
__determine_file_path
(
self
,
index
:
int
,
track
)
->
str
:
artists
=
track
[
'
track
'
][
'
artists
'
]
artists
=
track
[
'
track
'
][
'
artists
'
]
...
@@ -120,10 +142,10 @@ class SpotifyRecorder:
...
@@ -120,10 +142,10 @@ class SpotifyRecorder:
fileName
=
f
'
{
index
}
-
{
artists
}
-
{
track
[
"
track
"
][
"
name
"
]
}
.wav
'
fileName
=
f
'
{
index
}
-
{
artists
}
-
{
track
[
"
track
"
][
"
name
"
]
}
.wav
'
return
os
.
path
.
join
(
self
.
_destinationFolder
,
fileName
)
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
'
]
trackDurationInMs
=
track
[
'
track
'
][
'
duration_ms
'
]
trackDurationInSeconds
=
trackDurationInMs
//
1000
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
()
startTime
=
time
.
time
()
...
@@ -138,21 +160,22 @@ class SpotifyRecorder:
...
@@ -138,21 +160,22 @@ class SpotifyRecorder:
if
remainingTimeInMs
>
self
.
_THRESHOLD_TRACK_END_IN_MS
:
if
remainingTimeInMs
>
self
.
_THRESHOLD_TRACK_END_IN_MS
:
sleepTime
=
remainingTimeInSeconds
/
2
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
'
{
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
)
time
.
sleep
(
sleepTime
)
else
:
else
:
waitDuration
=
int
(
time
.
time
()
-
startTime
)
waitDuration
=
int
(
time
.
time
()
-
startTime
)
waitDuration
+=
timeWaitedForPlaying
if
waitDuration
<
trackDurationInSeconds
:
if
waitDuration
<
trackDurationInSeconds
-
self
.
_WAIT_TIME_TRACK_PLAYING_SHORT_IN_S
:
raise
RuntimeError
(
raise
RuntimeError
(
f
'
Track finished too early (waited:
{
waitDuration
}
s, expected:
{
trackDurationInSeconds
}
s)
'
)
f
'
Track finished too early (waited:
{
waitDuration
}
s, expected:
{
trackDurationInSeconds
}
s)
'
)
if
waitDuration
>
trackDurationInSeconds
+
self
.
_WAIT_TIME_TRACK_PLAYING_SHORT_IN_S
*
3
:
if
waitDuration
>
trackDurationInSeconds
+
self
.
_WAIT_TIME_TRACK_PLAYING_SHORT_IN_S
*
3
:
raise
RuntimeError
(
raise
RuntimeError
(
f
'
Track finished too late (waited:
{
waitDuration
}
s, expected:
{
trackDurationInSeconds
}
)
'
)
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
break
@staticmethod
@staticmethod
...
@@ -170,19 +193,31 @@ class SpotifyRecorder:
...
@@ -170,19 +193,31 @@ class SpotifyRecorder:
raise
RuntimeError
(
'
Device not found
'
)
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
])
self
.
_spotify
.
start_playback
(
device_id
=
deviceId
,
uris
=
[
trackUri
])
def
__wait_for_track_playing
(
self
)
->
None
:
def
__stop_playback_if_playing
(
self
,
deviceId
:
str
)
->
None
:
LOGGER
.
debug
(
f
'
Wait for track to start playing...
'
)
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
()
startTime
=
time
.
time
()
duration
=
0
while
time
.
time
()
-
startTime
<
self
.
_MAX_WAIT_TIME_TRACK_STARTING_IN_S
:
while
time
.
time
()
-
startTime
<
self
.
_MAX_WAIT_TIME_TRACK_STARTING_IN_S
:
if
self
.
_spotify
.
current_playback
()[
'
is_playing
'
]:
currentPlayback
=
self
.
_spotify
.
current_playback
()
LOGGER
.
debug
(
f
'
Track started playing after
{
time
.
time
()
-
startTime
:
.
1
f
}
s
'
)
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
break
else
:
raise
RuntimeError
(
f
'
Wrong track started playing (actual:
{
currentPlayback
[
"
item
"
][
"
id
"
]
}
, expected:
{
expectedTrackId
}
)
'
)
time
.
sleep
(
1
)
time
.
sleep
(
1
)
return
duration
if
__name__
==
'
__main__
'
:
if
__name__
==
'
__main__
'
:
with
open
(
'
config/settings-recorder.json
'
,
'
r
'
,
encoding
=
'
utf-8
'
)
as
f
:
with
open
(
'
config/settings-recorder.json
'
,
'
r
'
,
encoding
=
'
utf-8
'
)
as
f
:
...
@@ -196,7 +231,9 @@ if __name__ == '__main__':
...
@@ -196,7 +231,9 @@ if __name__ == '__main__':
SETTINGS
[
'
playlist
'
],
SETTINGS
[
'
playlist
'
],
SETTINGS
[
'
spotifyDeviceName
'
],
SETTINGS
[
'
spotifyDeviceName
'
],
SETTINGS
[
'
audioDeviceName
'
],
SETTINGS
[
'
audioDeviceName
'
],
SETTINGS
[
'
destinationFolder
'
])
SETTINGS
[
'
destinationFolder
'
],
SETTINGS
[
'
startNumber
'
],
SETTINGS
[
'
limit
'
],
)
spotifyBackup
.
run
()
spotifyBackup
.
run
()
...
...
This diff is collapsed.
Click to expand it.
config/settings-recorder-example.json
View file @
7effe35e
...
@@ -10,6 +10,8 @@
...
@@ -10,6 +10,8 @@
"spotifyDeviceName"
:
"MYDEVICE"
,
"spotifyDeviceName"
:
"MYDEVICE"
,
"audioDeviceName"
:
"3/4 - Musik (2- GIGAPort HD Audio driver) [Loopback]"
,
"audioDeviceName"
:
"3/4 - Musik (2- GIGAPort HD Audio driver) [Loopback]"
,
"destinationFolder"
:
""
,
"destinationFolder"
:
""
,
"startNumber"
:
1
,
"limit"
:
-1
,
"redirectUrl"
:
"http://localhost:8080"
,
"redirectUrl"
:
"http://localhost:8080"
,
"openBrowser"
:
false
,
"openBrowser"
:
false
,
"cacheFilePath"
:
".cache"
"cacheFilePath"
:
".cache"
...
...
This diff is collapsed.
Click to expand it.