<|start_filename|>assets/js/shuffle.js<|end_filename|>
function copy_array(array) {
    return array.slice(0);
}

function unbiased_shuffle(array) {
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
    }
}

function spread_shuffle(array, length, spread = 4, iterations) {
    // Partition array into chunks based on specified spread
    let partitions = [];
    let count = array.length;
    let partition_length = Math.floor(count / spread);
    for (let i = 1; i <= spread; i++) {
        if (i == spread) {
            partitions.push(array);
        } else {
            partitions.push(array.splice(0, partition_length));
        }
    }

    // Perform unbiased shuffle on partitions once
    for (let i = 0; i < partitions.length; i++) unbiased_shuffle(partitions[i]);

    // Exchange values between partitions randomly
    let modifications = iterations || count;
    for (let i = 0; i < modifications; i++) {
        // Choose 2 random partitions to exchange values
        let from = partitions[Math.floor(Math.random() * partitions.length)];
        let to = partitions[Math.floor(Math.random() * partitions.length)];

        // Choose 2 random indexes and their values from each partition
        let from_index = Math.floor(Math.random() * from.length);
        let to_index = Math.floor(Math.random() * to.length);
        let from_value = from[from_index];
        let to_value = to[to_index];

        // Exchange values between two indexes
        from[from_index] = to_value;
        to[to_index] = from_value;
    }

    if (length && length < count) {
        let results = [];
        let cursor = 0;
        let indexes = partitions.map(() => 0);
        for (let i = 0; i < length; i++) {
            let partition = partitions[cursor];
            let value = partition[indexes[cursor]];
            results.push(value);

            cursor++;
            if (cursor >= partitions.length) cursor = 0;

            indexes[cursor]++;
            if (indexes[cursor] >= partition.length) indexes[cursor] = 0;
        }

        return results;
    } else {
        return [].concat(partitions);
    }
}

async function shuffle_retrieve_songs(playlist_id, on_progress_change) {
    // Check local cache first
    let cache = spotify_profile.playlist_songs[playlist_id];
    if (cache) return cache;

    // Retrieve From Spotify API
    let token = get_access_token();
    let songs = await fetch_all_playlist_songs(token, playlist_id, on_progress_change);

    // Filter songs to only retain non local songs
    songs = songs.filter(({ is_local }) => is_local === false);

    // Cache resolved songs for faster access in future
    spotify_profile.playlist_songs[playlist_id] = songs;
    return songs;
}

function to_readable_date(date) {
    const month_names = [
        'January',
        'February',
        'March',
        'April',
        'May',
        'June',
        'July',
        'August',
        'September',
        'October',
        'November',
        'December',
    ];

    try {
        let chunks = date.split('-');
        let year = +chunks[0];
        let month = month_names[+chunks[1] - 1];
        let day = (+chunks[2]).toString();
        if (day.endsWith('1')) {
            day += 'st';
        } else if (day.endsWith('2')) {
            day += 'nd';
        } else if (day.endsWith('3')) {
            day += 'rd';
        } else {
            day += 'th';
        }

        return `${month} ${day}, ${year}`;
    } catch (error) {
        return 'Invalid Format';
    }
}

async function shuffle_and_play() {
    let playlist = $('#choose_playlist').val();
    let device = $('#choose_device').val();

    // Validate selected parameters
    let playlist_object = spotify_profile.playlists_by_key[playlist];
    if (playlist == 'default' || playlist_object == undefined)
        return alert('Please Select a Valid Playlist');
    if (device == 'default') return alert('Please Select a Device');

    // Mark Shuffle & Play button in flight
    let play_button = $('#play_button');
    play_button.text('Processing').prop('disabled', true).addClass('disabled');

    // Retrieve songs for playlist and create a copy to prevent modification of original cache
    let songs;

    // Attempt to retrieve songs from local cache first based on snapshot id
    let cache_lookup = await get_playlist_songs(playlist_object.id, playlist_object.snapshot_id);
    if (Array.isArray(cache_lookup)) {
        songs = cache_lookup;
    } else {
        // Retrieve songs fresh from Spotify API
        try {
            let total = playlist_object.tracks.total;
            let songs_lookup = await shuffle_retrieve_songs(playlist, (progress) =>
                play_button.text(`Retrieving Songs... [${progress} / ${total}]`)
            );

            // Cache songs locally for faster access times
            await store_playlist_songs(
                playlist_object.id,
                playlist_object.snapshot_id,
                songs_lookup
            );

            // Duplicate original songs array to prevent ordering of memory cache
            songs = copy_array(songs_lookup);
        } catch (error) {
            alert('Failed To Retrieve Songs From Spotify! Logged Error To Console');
            return console.log(error);
        }
    }

    // Shuffle songs based on length and only be left with 100 songs in the end as that is playback API limit
    play_button.text('Shuffling Songs');
    let uris_cap = 100;
    if (songs.length < uris_cap + 1) {
        unbiased_shuffle(songs);
    } else {
        // Determine shuffle spread based on a sample size of 50 song batches
        let sample_size = 50;
        let spread = Math.max(Math.floor(songs.length / sample_size), 4);
        songs = spread_shuffle(songs, uris_cap, spread);
    }

    // Map song uris for Spotify API
    let song_uris = songs.map((song) => song.track.uri);

    // Check for Spotify Premium With Shuffle Toggle
    play_button.text('Starting Playback');
    let is_premium = await toggle_playback_shuffle(get_access_token(), device, false);

    // Launch Playback If User Has Premium
    let reshuffle_message = 'Reshuffle & Play';
    if (is_premium) {
        await start_playback(get_access_token(), device, {
            uris: song_uris,
        });
    } else {
        // Retrieve temporary playlist
        let playlist_object = spotify_profile.temporary.playlist;

        // Create a new temporary playlist if one doesn't already exist
        if (playlist_object === null) {
            play_button.text('Creating Temporary Playlist');
            playlist_object = await create_playlist(
                get_access_token(),
                spotify_profile.id,
                spotify_profile.temporary.name,
                'An Automatic Playlist Generated By True Shuffle from ' +
                    location.origin +
                    location.pathname,
                false
            );

            spotify_profile.temporary.playlist = playlist_object;
        }

        // Update temporary playlist with shuffled song uris
        play_button.text('Updating Temporary Playlist');
        await replace_songs_in_playlist(get_access_token(), playlist_object.id, song_uris);

        reshuffle_message = 'Reshuffle & Update';
        $('#application_message')
            .html(
                `Your shuffled music has been placed inside a
        <strong>temporary</strong> playlist called
        <strong>True Shuffle Temporary</strong>.`
            )
            .show();
    }

    // Display shuffle results in application UI
    let shuffle_results = [];
    songs.forEach(({ track }) => {
        let title = track.name;
        let artists = track.artists.map((artist) => artist.name).join(', ');
        let image;
        if (Array.isArray(track.album.images)) image = track.album.images[0].url;

        shuffle_results.push(`
        <div class="row mt-3">
            <div class="song-element">
                ${
                    image
                        ? `<img
                class="song-image"
                src="${image}"
            />`
                        : ''
                }
                <p class="song-title">
                    ${title}<br /><strong class="song-subtitle"
                        >${artists} - Released On ${to_readable_date(
            track.album.release_date
        )}</strong
                    >
                </p>
            </div>
        </div>`);
    });

    $('#shuffle_results').html(shuffle_results.join('\n'));
    $('#shuffle_results_container').show();

    // Re-Enable UI controls for reshuffling
    return play_button.text(reshuffle_message).prop('disabled', false).removeClass('disabled');
}


<|start_filename|>assets/css/application.css<|end_filename|>
body {
    padding: 30px;
    background: white;
    overflow: hidden;
    color: white;
}

.page-container {
    width: 100%;
    height: 100%;
    background: linear-gradient(300deg, black, #070707);
    border-radius: 20px;
    box-shadow: 8px 6px 17px 10px rgb(0 0 0 / 20%);
}

.auth-container {
    padding: 3rem;
}

.landing-title {
    text-align: center;
    font-family: 'Poppins';
    font-weight: 600;
}

.landing-message {
    text-align: center;
    font-family: 'Poppins';
    font-weight: 400;
}

.auth-button {
    width: 100%;
    border-radius: 10px;
    background: black;
    color: white;
    border: 2px solid white;
    font-family: 'Poppins';
    font-weight: 600;
    padding: 20px;
    margin-top: 1rem;
    max-width: 410px;
    margin: auto;
    margin-top: 15px;
    font-size: 1.2rem;
    transition: all .5s;
    letter-spacing: 0px;
}

.auth-button:hover {
    letter-spacing: 0.5px;
    background: white;
    color: black;
}

.auth-button.disabled {
    opacity: 0.3;
    background: white!important;
    color: black!important;
    letter-spacing: 0px!important;
    user-select: none;
}

.page-select {
    display: block;
    padding: 1rem;
    background: black;
    border: 2px solid white;
    border-radius: 10px;
    color: white;
    max-width: 300px;
    margin: auto;
    outline: none;
}

.application-button {
    display: block;
    margin: auto;
    max-width: 300px;
    font-size: 15px;
    margin-top: 15px;
    padding: 15px;
}

.song-element {
    display: block;
    margin: auto;
    width: 100%;
    max-width: 750px;
    background: white;
    border-radius: 15px;
    padding: 10px;
}

.song-image {
    width: 55px;
    height: 55px;
    border-radius: 15px;
    float: left;
    box-shadow: 2px 2px 5px 3px rgba(0, 0, 0, 0.15);
}

.song-title {
    color: black;
    font-size: 1.3rem;
    float: left;
    margin-left: 20px;
    font-family: 'Poppins';
    font-weight: 600;
    margin-bottom: 0px;
    max-width: 88%;
}

.song-subtitle {
    font-size: 0.8rem;
    color: grey;
    font-weight: 600;
}

.github-icon {
    width: 50px;
    height: 50px;
    border-radius: 10px;
    background: white;
    float: right;
    position: absolute;
    right: 50px;
    bottom: 50px;
    cursor: pointer;
    box-shadow: 0px 0px 25px 2px rgb(255 255 255 / 10%);
    transition: all 0.3s;
}

.github-icon-img {
    display: block;
    width: 32px;
    height: 32px;
    margin-left: auto;
    margin-right: auto;
    margin-top: 9px;
}

.github-icon:hover {
    bottom: 53px;
    box-shadow: 0px 0px 25px 2px rgb(255 255 255 / 20%);
}

@media screen and (max-width: 540px) {
    body {
        padding: 15px;
    }
    .auth-container {
        padding: 3rem;
        padding-left: 0.5rem;
        padding-right: 0.5rem;
    }
    .song-title {
        font-size: 0.9rem;
        max-width: 75%;
    }
    .song-image {
        width: 45px;
        height: 45px;
    }
    .song-subtitle {
        font-size: 0.6rem;
    }
    .github-icon {
        display: none;
    }
}

<|start_filename|>assets/js/storage.js<|end_filename|>
const LocalDB = new Dexie('PlaylistSongs');

LocalDB.version(1).stores({
    songs: '++id,playlist_id,snapshot_id,data',
});

async function store_playlist_songs(playlist_id, snapshot_id, songs) {
    // Cache songs to local IndexedDB database
    await LocalDB.songs.add({
        playlist_id: playlist_id,
        snapshot_id: snapshot_id,
        data: JSON.stringify(songs),
    });
}

let _songs_cache;

async function get_playlist_songs(playlist_id, snapshot_id, auto_expire = true) {
    // Retrive data from IndexedDB on first lookup
    if (_songs_cache == undefined)
        _songs_cache = (await LocalDB.songs.toArray()).map((cell) => {
            cell.data = JSON.parse(cell.data);
            return cell;
        });

    // Iterate through cache to match against playlist id and snapshot id
    let result;
    _songs_cache.forEach(async (cell) => {
        let plist_check = cell.playlist_id === playlist_id;
        let snapshot_check = cell.snapshot_id === snapshot_id;
        if (plist_check) {
            if (snapshot_check) {
                result = cell;
            } else if (auto_expire) {
                try {
                    await LocalDB.songs.delete(cell.id);
                    console.log(`[SONG_STORAGE] Cleaned Up Songs Cache ID: ${cell.id}`);
                } catch (error) {
                    console.log(`[SONG_STORAGE] Failed To Cleanup Expired Songs @ ID: ${cell.id}`);
                    console.log(error);
                }
            }
        }
    });

    if (result) return result.data;
}


<|start_filename|>assets/js/main.js<|end_filename|>
const spotify_client_id = '0e188cfad9f3470ca424b84c2dc532df'; // Change this in your setup to your own Spotify OAuth application clientid

function random_hex(size) {
    return [...Array(size)]
        .map(() => Math.floor(Math.random() * 16).toString(16))
        .join('');
}

function has_local_storage() {
    try {
        localStorage.setItem('ls_test', 'true');
        localStorage.removeItem('ls_test');
        return true;
    } catch (err) {
        return false;
    }
}

function handle_auth_callback() {
    // Ensure hash is from Spotify
    if (location.hash.indexOf('#access_token=') == -1)
        return (location.hash = '');

    // Parse hash parameters
    let payload = {};
    location.hash
        .replace('#', '')
        .split('&')
        .forEach((chunk) => {
            chunk = chunk.split('=');
            if (chunk.length == 2) {
                payload[chunk[0]] = decodeURIComponent(chunk[1]);
            }
        });

    // Ensure a access_token and state is returned
    if (
        typeof payload.access_token !== 'string' ||
        typeof payload.state !== 'string' ||
        isNaN(+payload.expires_in)
    )
        return;

    // Validate authentication hash
    if (!verify_auth_hash(payload.state)) return (location.hash = '');

    // Store access_token and it's expiry in localStorage for persisted session
    localStorage.setItem('access_token', payload.access_token);
    localStorage.setItem(
        'access_token_expiry',
        (Date.now() + +payload.expires_in * 1000).toString()
    );
    location.hash = '';

    // Mark user device as returning_user for faster authentication
    localStorage.setItem('returning_user', 'true');
}

let spotify_session;
function get_access_token(reload_on_fail = true) {
    if (spotify_session) {
        const { token, expiry } = spotify_session;
        if (Date.now() < expiry) return token;
    } else {
        let token = localStorage.getItem('access_token');
        let expiry = +localStorage.getItem('access_token_expiry');
        if (Date.now() < expiry) {
            spotify_session = {
                token,
                expiry,
            };

            return token;
        }
    }

    if (reload_on_fail) {
        alert(
            'You have been inactive for too long. Please connect your account again.'
        );
        location.reload();
        throw new Error('Reloading Page'); // Throw this to prevent calling API request from being made
    }
}

function get_auth_hash(expiry_seconds = 5) {
    let hash = random_hex(20);
    localStorage.setItem(
        'auth_hash',
        `${hash}:${Date.now() + 1000 * expiry_seconds}`
    );
    return hash;
}

function verify_auth_hash(hash) {
    let lookup = localStorage.getItem('auth_hash');
    if (typeof lookup == 'string') {
        let chunks = lookup.split(':');
        if (chunks.length == 2) {
            let chunk_hash = chunks[0];
            let chunk_expiry = +chunks[1];
            if (chunk_hash == hash && Date.now() < chunk_expiry) return true;
        }
    }

    return false;
}

function connect_with_spotify() {
    let hash = get_auth_hash(60);
    let oauth_url = `https://accounts.spotify.com/authorize?client_id=${spotify_client_id}&response_type=token&redirect_uri=${encodeURIComponent(
        location.origin + location.pathname
    )}&state=${hash}&scope=${encodeURIComponent(
        'playlist-modify-public playlist-modify-private playlist-read-private playlist-read-collaborative user-modify-playback-state user-library-read user-read-playback-state'
    )}`;

    $('#connect_button').text('Redirecting').addClass('disabled');
    location.href = oauth_url;
}

function application_loader(visible, message) {
    let loader = $('#loader_container');
    let message_tag = $('#loader_message');
    let is_visible = loader.css('display') !== 'none';

    if (typeof message == 'string') message_tag.text(message);

    if (visible && !is_visible) {
        loader.show();
    } else if (!visible && is_visible) {
        loader.hide();
    }
}

let spotify_profile = {
    id: '',
    devices: [],
    playlists: [],
    playlists_by_key: {},
    playlist_songs: {},
    temporary: {
        name: 'Temporary True Shuffle',
        playlist: null,
    },
};

async function load_application() {
    let token = get_access_token();

    // Retrieve user's spotify active devices for playback
    application_loader(true, 'Retrieving Your Spotify Devices...');
    spotify_profile.devices = await fetch_devices(token);

    // Retrieve user's spotify playlists for choice of music
    application_loader(true, 'Retrieving Your Spotify Playlists...');
    let { userId, playlists } = await fetch_all_playlists(token);
    spotify_profile.id = userId;

    // Sort user's spotify playlists from most to least tracks
    spotify_profile.playlists = playlists.sort(
        (a, b) => b.tracks.total - a.tracks.total
    );

    // Store playlists by key:value where key is playlist id for faster access
    spotify_profile.playlists.forEach((playlist) => {
        spotify_profile.playlists_by_key[playlist.id] = playlist;
        if (playlist.name === spotify_profile.temporary.name)
            spotify_profile.temporary.playlist = playlist;
    });

    // Check if a valid device is available before enabling application
    if (spotify_profile.devices.length == 0) {
        $('#application_message')
            .html(
                `Please refresh the page when you have an active device with the Spotify application open. If you still do not see your device, you can play/pause any random song in your spotify application and then refresh this page.`
            )
            .show();

        $('#play_button')
            .text('No Devices Available')
            .addClass('disabled')
            .prop('disabled', true);

        $('#application_section').show();
        return application_loader(false, '');
    }

    // Update UI with playlist choices
    let playlists_dom = [];
    spotify_profile.playlists.forEach((playlist) =>
        playlists_dom.push(
            `<option value="${playlist.id}">${playlist.name} - ${playlist.tracks.total} Songs</option>`
        )
    );
    $('#choose_playlist').html(playlists_dom.join('\n'));

    // Update UI with device choices for playback
    let devices_dom = [];
    spotify_profile.devices.forEach((device) =>
        devices_dom.push(`<option value="${device.id}">${device.name}</option>`)
    );
    $('#choose_device').html(devices_dom.join('\n'));

    // Disable UI loader and show application UI
    application_loader(false);
    $('#application_section').show();
}

window.addEventListener('load', () => {
    // Ensure localStorage is available else browser is unsupported
    if (!has_local_storage())
        return $('#connect_button')
            .text('Unsupported Browser')
            .addClass('disabled');

    // Attempt to parse hash parameters from spotify for oauth callback
    handle_auth_callback();

    // Show application flow if a valid access token exists
    if (typeof get_access_token(false) == 'string') {
        $('#auth_section').hide();
        return load_application();
    }

    // Check and redirect user to oauth if they are returning
    let is_recurring_user = localStorage.getItem('returning_user') === 'true';
    if (is_recurring_user) connect_with_spotify();
});


<|start_filename|>assets/js/spotify.js<|end_filename|>
async function async_wait(milliseconds) {
    return new Promise((resolve, _) =>
        setTimeout((r) => r(), milliseconds, resolve)
    );
}

async function get_user_profile(token) {
    let response = await fetch('https://api.spotify.com/v1/me', {
        method: 'GET',
        headers: {
            authorization: 'Bearer ' + token,
        },
    });

    if (response.status !== 200) throw new Error('Bad HTTP Status Code');

    let body = await response.json();
    return body;
}

async function fetch_devices(token) {
    let response = await fetch('https://api.spotify.com/v1/me/player/devices', {
        method: 'GET',
        headers: {
            authorization: 'Bearer ' + token,
        },
    });

    if (response.status !== 200) throw new Error('Invalid HTTP Status Code');

    let body = await response.json();
    return body.devices;
}

async function fetch_all_playlists(token, limit = 50, offset = 0, delay = 150) {
    let response = await fetch(
        `https://api.spotify.com/v1/me/playlists?limit=${limit}&offset=${offset}`,
        {
            method: 'GET',
            headers: {
                authorization: 'Bearer ' + token,
            },
        }
    );

    if (response.status !== 200) throw new Error('Invalid HTTP Status Code');

    let body = await response.json();
    let playlists = body.items;

    // Paginate further if more playlists need to be fetched
    if (typeof body.next == 'string') {
        // Delay to prevent hitting rate limit
        await async_wait(delay);
        let paginated = await fetch_all_playlists(
            token,
            limit,
            body.offset + limit
        );

        // Concatenate original results with paginated results
        playlists = playlists.concat(paginated.playlists);
    }

    return {
        userId: body.href.split('users/')[1].split('/')[0],
        playlists: playlists,
    };
}

async function fetch_all_playlist_songs(
    token,
    playlist_id,
    on_progess_change,
    limit = 50,
    offset = 0,
    delay = 250
) {
    let response = await fetch(
        `	https://api.spotify.com/v1/playlists/${playlist_id}/tracks?limit=${limit}&offset=${offset}`,
        {
            method: 'GET',
            headers: {
                authorization: 'Bearer ' + token,
            },
        }
    );

    if (response.status !== 200) throw new Error('Invalid HTTP Status Code');

    let body = await response.json();
    let songs = body.items;
    if (typeof on_progess_change == 'function')
        on_progess_change(body.offset + songs.length);

    // Paginate further if more songs need to be fetched
    if (typeof body.next == 'string') {
        // Delay to prevent hitting rate limit
        await async_wait(delay);
        songs = songs.concat(
            await fetch_all_playlist_songs(
                token,
                playlist_id,
                on_progess_change,
                limit,
                body.offset + limit
            )
        );
    }

    return songs;
}

async function create_playlist(
    token,
    userId,
    name,
    description = '',
    public = false
) {
    let response = await fetch(
        `https://api.spotify.com/v1/users/${userId}/playlists`,
        {
            method: 'POST',
            headers: {
                authorization: 'Bearer ' + token,
                'content-type': 'application/json',
            },
            body: JSON.stringify({
                name,
                description,
                public,
            }),
        }
    );

    return await response.json();
}

async function append_songs_to_playlist(token, playlist_id, song_uris) {
    let response = await fetch(
        `https://api.spotify.com/v1/playlists/${playlist_id}/tracks`,
        {
            method: 'POST',
            headers: {
                authorization: 'Bearer ' + token,
                'content-type': 'application/json',
            },
            body: JSON.stringify({
                uris: song_uris,
            }),
        }
    );

    let body = await response.json();

    if (response.status !== 201) {
        console.log(body);
        throw new Error('Bad HTTP Status Code');
    }

    return body;
}

async function replace_songs_in_playlist(token, playlist_id, song_uris) {
    let response = await fetch(
        `https://api.spotify.com/v1/playlists/${playlist_id}/tracks`,
        {
            method: 'PUT',
            headers: {
                authorization: 'Bearer ' + token,
                'content-type': 'application/json',
            },
            body: JSON.stringify({
                uris: song_uris,
            }),
        }
    );

    let body = await response.json();

    if (response.status !== 201) {
        console.log(body);
        throw new Error('Bad HTTP Status Code');
    }

    return body;
}

async function toggle_playback_shuffle(token, device_id, state = false) {
    let response = await fetch(
        `https://api.spotify.com/v1/me/player/shuffle?device_id=${device_id}&state=${
            state === true ? 'true' : 'false'
        }`,
        {
            method: 'PUT',
            headers: {
                authorization: 'Bearer ' + token,
            },
        }
    );

    if (response.status === 403) return false;

    if (response.status === 204) {
        return true;
    } else {
        throw new Error('Bad HTTP Status Code');
    }
}

async function start_playback(
    token,
    device_id,
    { context_uri, uris, offset, position_ms }
) {
    let response = await fetch(
        `https://api.spotify.com/v1/me/player/play?device_id=${device_id}`,
        {
            method: 'PUT',
            headers: {
                authorization: 'Bearer ' + token,
                'content-type': 'application/json',
            },
            body: JSON.stringify({
                context_uri,
                uris,
                offset,
                position_ms,
            }),
        }
    );

    if (response.status === 204) {
        return true;
    } else {
        throw new Error('Bad HTTP Status Code');
    }
}
