From 1bce2895510ce6a730976c84d8757db0121d6c35 Mon Sep 17 00:00:00 2001 From: Cyb10101 Date: Sat, 25 Feb 2023 17:16:29 +0100 Subject: [PATCH 01/23] Move JavaScript and Style Sheets to a file --- app.css | 106 ++++++++ app.js | 582 ++++++++++++++++++++++++++++++++++++++++++++ index.html | 698 +---------------------------------------------------- 3 files changed, 690 insertions(+), 696 deletions(-) create mode 100644 app.css create mode 100644 app.js diff --git a/app.css b/app.css new file mode 100644 index 0000000..b441ca8 --- /dev/null +++ b/app.css @@ -0,0 +1,106 @@ +body { + padding: 0; + margin: 0; + font-family: 'Lato', sans-serif; + background: url(./gplaypattern.png) repeat; + background-size: 188px 178px; +} + +.container { + width: 640px; + margin-left: auto; + margin-right: auto; +} +.footer { + position: fixed; + bottom: 0; + width: 100%; +} + +.heading { + color: #444; + font-size: 24px; + background-color: rgba(255,255,255,0.9); + border-bottom: solid 1px #eaeaea; + width: 100%; + z-index: 500; + padding: 12px; +} + +.button-main { + font-size: 18px; + font-weight: 600; + line-height: 1.5; + color: #fff; + background-color: #2ebd59; + display: inline-block; + margin-bottom: 0; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + border-radius: 500px; + padding: 12px 47px 8px; + border-width: 0; + letter-spacing: 1.2px; + min-width: 130px; + text-transform: uppercase; + white-space: normal; + margin: 0 0 1em; +} +.button-big { + margin-left: 150px; +} +.button-medium { + margin-left: 80px; +} + +.title-bold { + font-weight: bold; + font-size: 44px; +} +.title-light { + font-weight: 300; + color: #555; + font-size: 44px; +} +.title-medium { + font-weight: bold; + font-size: 18px; +} + +.progress-bar { + width: 0%; + background-color: #86e01e; + position: relative; + height: 16px; + border-radius: 4px; + -webkit-transition: 0.4s linear; + -moz-transition: 0.4s linear; + -o-transition: 0.4s linear; + transition: 0.4s linear; + -webkit-transition-property: width, background-color; + -moz-transition-property: width, background-color; + -o-transition-property: width, background-color; + transition-property: width, background-color; + -webkit-box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25), inset 0 1px rgba(255, 255, 255, 0.1); + box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.25), inset 0 1px rgba(255, 255, 255, 0.1); +} + +.progress { + width: 100%; + padding: 4px; + margin-top: 10px; +} + +.link { + font-size: 14px; + padding: 5px 15px; + color: #121F59; + text-decoration: none; +} +.link:hover { + color: #A2C852; +} diff --git a/app.js b/app.js new file mode 100644 index 0000000..ceaf63a --- /dev/null +++ b/app.js @@ -0,0 +1,582 @@ +var conf = config; + +var authWindow = null; +var token = null; +var userId = ''; +var collections = {}; +var name = 'spotify'; + +var isImporting = false; +var isExporting = false; +var globalStep = ""; +var playlistStep = 0; +var trackStep = 0; +var trackTotal = 0; + +var playlistQueue = []; +var savedQueue = []; + +var makingChanges = false; + +function refreshTrackData(callback) { + if (!isExporting && !isImporting) { + isExporting = true; + resetCounter(); + $('#pnlLoadingAccount').show(); + $('#loadingTitle').html('Please wait. Loading your playlists and tracks ...'); + refreshPlaylist(function () { + refreshMyMusicTracks(function () { + // refreshStarredTracks(function () { + $('#loadingTitle').html('Finished loading, you now might want to export or import.'); + isExporting = false; + callback(); + // }); + }); + }); + } +} + +function resetCounter() { + globalStep = ''; + playlistStep = 0; + playlistTotal = 0; + trackStep = 0; + trackTotal = 0; +} + +function refreshProgress() { + $('#globalStep').html(globalStep); + $('#playlistStep').html(playlistStep); + $('#playlistTotal').html(playlistTotal); + $('#trackStep').html(trackStep); + $('#trackTotal').html(trackTotal); + var progress = 0; + if (trackTotal > 0) { + var progress = Math.floor(((trackStep / trackTotal) * 100)); + } + $('#progressBar').css('width', progress+'%') + if (typeof collections !== 'undefined' && !makingChanges) { + var set = collectionProperties(collections); + $('#loadingPlaylists').html(""+set.playlistCount+" playlists"); + $('#loadingTracks').html(""+set.trackCount+" tracks"); + } + if (typeof importColl !== 'undefined') { + var set2 = collectionProperties(importColl); + $('#filePlaylists').html(""+set2.playlistCount+" playlists"); + $('#fileTracks').html(""+set2.trackCount+" tracks"); + } + setTimeout(refreshProgress, 1000); +} + +function login() { + var width = 480, height = 640; + var left = (screen.width / 2) - (width / 2); + var top = (screen.height / 2) - (height / 2); + + var set = { + client_id: conf.client_id, + redirect_uri: conf.redirect_uri, + scope: 'playlist-read playlist-read-private playlist-modify-public playlist-modify-private user-library-read user-library-modify', + response_type: 'token', + show_dialog: 'true' + }; + authWindow = window.open( + "https://accounts.spotify.com/authorize?" + urlEncodeSet(set), + "Spotify", + 'menubar=no,location=no,resizable=no,scrollbars=no,status=no, width=' + width + ', height=' + height + ', top=' + top + ', left=' + left + ); +} + +function authCallback(event){ + if (event.origin !== conf.uri) { + console.log("config.uri missconfigured:"); + console.log({ uri: conf.uri, origin: origin }); + return; + } + if (authWindow) { + authWindow.close(); + } + handleAuth(event.data); +} + +function urlEncodeSet(set) { + var comps = []; + for (var i in set) { + if (set.hasOwnProperty(i)) { + comps.push(encodeURIComponent(i)+"="+encodeURIComponent(set[i])); + } + } + var string = comps.join("&"); + return string; +} + +function download(filename, text) { + var pom = document.createElement('a'); + pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); + pom.setAttribute('download', filename); + + if (document.createEvent) { + var event = document.createEvent('MouseEvents'); + event.initEvent('click', true, true); + pom.dispatchEvent(event); + } + else { + pom.click(); + } +} + +function readFile(evt) { + //Retrieve the first (and only!) File from the FileList object + var f = evt.target.files[0]; + + if (f) { + $('#fileName').html(f.name); + + var r = new FileReader(); + r.onload = function(e) { + var json = e.target.result; + + importColl = JSON.parse(json); + + $('#pnlFile').hide(); + $('#pnlFileInfo').show(); + $('#pnlUpload').show(); + + compareEverything(); + } + r.readAsText(f); + } else { + alert("Failed to load file"); + } +} + +function collectionProperties(coll) { + return { playlistCount: collPlaylistCount(coll), trackCount: collTrackCount(coll) }; +} + +function collTrackCount(coll) { + var count = 0; + var keys = _.keys(coll.playlists); + $.each(keys, function (index, value) { + count += coll.playlists[value].tracks.length; + }); + if (coll.starred) { + count += coll.starred.length; + } + if (coll.saved) { + count += coll.saved.length; + } + return count; +} + +function collPlaylistCount(coll) { + var keys = _.keys(coll.playlists); + var count = keys.length + 1; + if (!("importedStarred" in keys)) { + count++; + } + return count; +} + +function compareEverything() { + if (!isImporting && !isExporting) { + isImporting = true; + makingChanges = true; + resetCounter(); + + savedQueue = []; + playlistQueue = []; + + globalStep = "Uploading"; + if (typeof importColl !== 'undefined') { + + playlistTotal = collPlaylistCount(importColl); + + // TONOTDO:compare starred -> can not really do that since there is no api to manipulate those + // instead we just create a replacement-standard-list + globalStep = "Comparing starred tracks"; + + makeSureImportedStarredExists(function (proceed) { + if (importColl.starred && importColl.starred.length > 0) { + compareUriTracks(importColl.starred, collections.starred, addToStarred); + } + // compare saved + globalStep = "Comparing saved tracks"; + compareIdTracks(importColl.saved, collections.saved, addToSaved); + playlistStep += 1; + + // compare other playlists + var playlistNames = _.keys(importColl.playlists); + globalStep = "Comparing custom playlists"; + handlePlaylistCompare(playlistNames.reverse(), function () { + handleTrackUpload(); + }); + }); + } + } +} + +function handleTrackUpload() { + + trackDiff = savedQueue.length + playlistQueue.length; + trackTotal = Math.max(collTrackCount(importColl), trackDiff); + trackStep = trackTotal - trackDiff; + + + if (trackTotal > 0) { + $('#progressBar').show(); + globalStep = "Uploading tracks"; + handleSavedRequests(savedQueue.reverse(), function () { + handlePlaylistRequests(playlistQueue.reverse(), function () { + globalStep = "Finished uploading"; + trackTotal = trackStep; + isImporting = false; + }); + }); + } else { + globalStep = "No new tracks found in import"; + } +} + +function handlePlaylistCompare(names, callback) { + var name = names.pop(); + if (!name) { + callback(); + return; + } + makeSurePlaylistExists(name, function (proceed) { + if (proceed) { + var playlistId = collections.playlists[name].id; + compareUriTracks(importColl.playlists[name].tracks, collections.playlists[name].tracks, function (uri) { + addToPlaylist(playlistId, uri); + }); + } + handlePlaylistCompare(names, callback); + }); +} + +function addToPlaylist(playlistId, trackUri) { + playlistQueue.push('https://api.spotify.com/v1/users/' + userId + '/playlists/' + playlistId + '/tracks?uris=' + encodeURIComponent(trackUri)); +} + +function makeSurePlaylistExists(name, callback) { + playlistStep += 1; + if (name in collections.playlists) { + callback(true); + return; + } + var set = { name: name, public: "true" }; + $.ajax({ + method: "POST", + url: 'https://api.spotify.com/v1/users/' + userId + '/playlists', + data: JSON.stringify(set), + contentType: 'application/json', + headers: { + 'Authorization': 'Bearer ' + token + }, + success: function(response) { + // alert(JSON.stringify(response)); + collections.playlists[response.name] = { + name: response.name, + href: response.tracks.href, + id: response.id, + tracks: [] + }; + callback(true); + }, + fail: function () { + callback(false); + } + }); +} + +function makeSureImportedStarredExists(callback) { + var name = 'importedStarred'; + makeSurePlaylistExists(name, callback); +} + +function addToStarred(trackUri) { + var name = 'importedStarred'; + var playlistId = collections.playlists[name].id; + uriInTracks(trackUri, collections.playlists[name].tracks, function (uri) { + addToPlaylist(playlistId, uri); + }); +} + +function handleSavedRequests(arr, callback) { + var url = arr.pop(); + if (url) { + trackStep += 1; + $.ajax({ + method: "PUT", + url: url, + headers: { + 'Authorization': 'Bearer ' + token + }, + success: function () { + }, + fail: function (jqXHR, textStatus, errorThrown) { + console.log(errorThrown); + } + }) + .always(function () { + handleSavedRequests(arr, callback); + }); + } else { + callback(); + } +} + + +function handlePlaylistRequestsWithTimeout(arr, callback) { + setTimeout(function() { console.log("Fast runners are dead runners"); handlePlaylistRequests(arr, callback) }, conf.slowdown_import); +} + +function handlePlaylistRequests(arr, callback) { + var url = arr.pop(); + if (url) { + trackStep += 1; + $.ajax({ + method: "POST", + url: url, + contentType: 'application/json', + headers: { + 'Authorization': 'Bearer ' + token + }, + success: function(response) { + // collections.playlists[response.name] = { + // id: response.id, + // uri: response.uri + // }; + }, + fail: function (jqXHR, textStatus, errorThrown) { + console.log(errorThrown); + } + }) + .always(function () { + handlePlaylistRequestsWithTimeout(arr, callback); + }) + } else { + callback(); + } +} + +function uriInTracks(uri, tracks, addCallback) { + var found = false; + $.each(tracks, function (index, value) { + if (value.uri == uri) { + found = true; + } + }); + if (!found) { + addCallback(uri); + } +} + +function addToSaved(id) { + savedQueue.push('https://api.spotify.com/v1/me/tracks?ids='+id); +} + +function compareUriTracks(imported, stored, addCallback) { + $.each(imported, function (index, value) { + var found = false; + $.each(stored, function (index2, value2) { + if (value.uri == value2.uri) { + found = true; + } + }); + if (!found) { + addCallback(value.uri); + } + }); +} + +function compareIdTracks(imported, stored, addCallback) { + $.each(imported, function (index, value) { + var found = false; + $.each(stored, function (index2, value2) { + if (value.id == value2.id) { + found = true; + } + }); + if (!found) { + addCallback(value.id); + } + }); +} + +function bindControls() { + $('#btnImport').click(function () { + $('#pnlAction').hide(); + $('#pnlImport').show(); + }); + $('#btnExport').click(function () { + var json = JSON.stringify(collections); + var d = new Date(); + var time = '@' + d.getFullYear() + '_' + (d.getMonth() + 1) + '_' + d.getDate(); + download(name+time+'.json', json); + }); + $('#fileImport').change(readFile); +} + +function handleAuth(accessToken) { + token = accessToken; + // fetch my public playlists + $.ajax({ + url: 'https://api.spotify.com/v1/me', + headers: { + 'Authorization': 'Bearer ' + accessToken + }, + success: function(response) { + var user_id = response.id.toLowerCase(); + userId = user_id; + name = user_id; + + $('#userName').html(name); + $('#pnlLoggedOut').hide(); + + refreshTrackData(function () { + $('#pnlAction').show(); + }); + } + }); +} + +function refreshMyMusicTracks(callback) { + collections.saved = []; + playlistStep += 1; + loadTrackChunks('https://api.spotify.com/v1/me/tracks', collections.saved, callback); +} + +// DEPRECATED +// function refreshStarredTracks(callback) { +// collections.starred = []; +// playlistStep += 1; +// loadStarred('https://api.spotify.com/v1/users/' + userId + '/starred', collections.starred, callback) +// } + +// DEPRECATED +// function loadStarred(url, arr, callback) { +// $.ajax({ +// url: url, +// headers: { +// 'Authorization': 'Bearer ' + token +// }, +// success: function(data) { +// if (!data) return; +// if ('tracks' in data) { +// loadTrackChunks(data.tracks.href, arr, callback); +// } else { +// callback(); +// } +// } +// }); +// } + +function loadTrackChunksWithTimeout(url, arr, callback, timeout) { + setTimeout(function() { console.log("Taking breath, not to fast my cheetah"); loadTrackChunks(url, arr, callback) }, conf.slowdown_export); +} + +function loadTrackChunks(url, arr, callback) { + $.ajax({ + url: url, + headers: { + 'Authorization': 'Bearer ' + token + }, + success: function (data) { + if (!data) return; + if ('items' in data) { + $.each(data.items, function (index, value) { + if(value.track !== null){ + arr.push({ id: value.track.id, uri: value.track.uri }); + }else{ + console.log("track is null", value); + } + }); + } else { + arr.push({ id: data.track.id, uri: data.track.uri }); + } + if (data.next) { + loadTrackChunksWithTimeout(data.next, arr, callback); + } else { + callback(); + } + } + }); +} + +function refreshPlaylist(callback) { + collections.playlists = {}; + var playlists = []; + loadPlaylistChunks('https://api.spotify.com/v1/users/' + userId + '/playlists', playlists, function () { + handlePlaylistTracks(playlists, collections.playlists, callback); + }); +} + +function loadPlaylistChunks(url, arr, callback) { + $.ajax({ + url: url, + headers: { + 'Authorization': 'Bearer ' + token + }, + success: function (data) { + if (!data) return; + if ('items' in data) { + $.each(data.items, function (index, value) { + if (value.tracks && value.tracks.href) { + arr.push({ + name: value.name, + href: value.tracks.href, + id: value.id, + tracks: [] + }); + } + }); + } else { + if (data.tracks && data.tracks.href) { + arr.push({ + name: data.name, + href: data.tracks.href, + id: value.id, + tracks: [] + }); + } + } + if (data.next) { + loadPlaylistChunks(data.next, arr, callback); + } else { + callback(); + } + } + }); +} + +function handlePlaylistTracks(arr, result, callback) { + var item = arr.pop(); + if (!item) { + return callback(); + } + playlistStep += 1; + item.tracks = []; + loadTrackChunks(item.href, item.tracks, function () { + delete item.href; + result[item.name] = item; + if (arr.length == 0) { + callback(); + } else { + handlePlaylistTracks(arr, result, callback); + } + }); +} + +window.onload = function() { + if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) { + // MSIE + $('#pnlLoggedOut').html('Please use Firefox or Chrome, due to a bug in Internet Explorer'); + } else { + $('#login').click(login); + window.addEventListener("message", authCallback, false); + bindControls(); + refreshProgress(); + } +} diff --git a/index.html b/index.html index 5b89408..70d8632 100644 --- a/index.html +++ b/index.html @@ -4,119 +4,11 @@ SpotMyBackup + - - Fork me on GitHub @@ -180,592 +72,6 @@ Impress - + From 3787b88b566ec243f33ae6500d02bae5b2fee4e0 Mon Sep 17 00:00:00 2001 From: Cyb10101 Date: Sat, 25 Feb 2023 17:18:23 +0100 Subject: [PATCH 02/23] Adjust line indentation --- index.html | 124 ++++++++++++++++++++++++++--------------------------- login.html | 41 ++++++++---------- 2 files changed, 79 insertions(+), 86 deletions(-) diff --git a/index.html b/index.html index 70d8632..d3b3a41 100644 --- a/index.html +++ b/index.html @@ -1,77 +1,75 @@ - - - - SpotMyBackup + + + SpotMyBackup - - - - - - - - Fork me on GitHub + + + + + + + + Fork me on GitHub -
-
- SpotMyBackup +
+
+ SpotMyBackup +
+
+
+ Export your playlists and tracks setup into a file by a single click. Import the file to restore your setup at any time.

-
- Export your playlists and tracks setup into a file by a single click. Import the file to restore your setup at any time. -
-
- -
+
- + +