Skip to content

Pavel's changes #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.DS_Store
*.csv
.ipynb_checkpoints
.~*
27 changes: 13 additions & 14 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
os: osx
language: python
python:
- 3.6

before_install:
- brew cask install phantomjs
- git clone git://github.com/n1k0/casperjs.git casperjs
- export PATH=`pwd`/casperjs/bin/:$PATH

before_script:
- phantomjs --version
- casperjs --version
- python -m SimpleHTTPServer 8080 &
- sleep 10
install:
- pip install notebook

script:
- casperjs test test/integration
- echo "Be excellent to each other!"
- python -m nbconvert --to html taste_analysis.ipynb

# Aight, this is definitively how you do these encrypted keys: `sudo apt install ruby ruby-dev libz-dev`,
# `gem install travis`, make sure you're in a folder with a .travis.yml, `travis encrypt thekey123456abcde`, and put
# the result under the thing it's supposed to represent. No VARIABLE=key, no worrying about capitalization or other
# command line flags, no putting the raw key in here lest github sniff it out and delete your token.
deploy:
provider: pages
github_token:
secure: "qhlPRkjNVw+KihpRoC/UBTaFOhtJD+FyY8V5eLozUxZEYkj/WkfJXyodSrUonDZpi3ycozpv0lUIbdOdW5SV13ewzf4PlOKXRCkjwS25P4dUiY1J8dbTfaeOuFWR4LozABt/TFenxPhocKPBmDTIN9i+R5EHdYfjMoAqs8uPZMgaf6Rzxah/Bwtl5+syEQI1ploFNKdP08Yc8MBsnZP2CJqNFpPwv4HfwncBjOHLldaKYFewG5j28L+E1qjOg0WVrwIe6weql9isg0jObEvmupFmxoh4vdJOVG/V+hsU29z0s0I+ZwmmVKskzT0NY1NvrM82xAr35fLUFF6Df6YtA9/KBIB5QHtDt6aZ2Tec6rhwzB5y3QwLF3RH5P3A+8cKLf2Z061FdMKQQ3JIUe3OMkXFtPGxJY42RJoqxfltL1gjYhGXQR9uhAh7BCafbx1NOXGbAOlcU/w82Ant+rJDofQXAMTUtmeZeVoG2lkztKwOwNY7TYBXhmlX+g8UNLG3f063ekJ+2Qfmls082masnjUycpYLdVX/syW/8iPhWT+HbwLpgBks7Fb8CAvXQHa4p9bnw80IjAtDwIH56EYHGp+BaRu6mwSAlPpdOPEyzZKisRf90uLo1XYO+VSPp2/JcHIo7aaXTkxG8s3HJ2gWKS4IRg5oHa/z2uRt6ZxDRH4="
skip_cleanup: true
github_token: $GITHUB_TOKEN
keep_history: true
on:
branch: master
1 change: 1 addition & 0 deletions CNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exportify.net
186 changes: 186 additions & 0 deletions FileSaver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* FileSaver.js
* A saveAs() FileSaver implementation.
*
* By Eli Grey, http://eligrey.com
*
* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
* source : http://purl.eligrey.com/github/FileSaver.js
*/

(function (global, factory) {
if (typeof define === "function" && define.amd) {
define([], factory);
} else if (typeof exports !== "undefined") {
factory();
} else {
var mod = {
exports: {}
};
factory();
global.FileSaver = mod.exports;
}
})(this, function () {
"use strict";

// The one and only way of getting global scope in all environments
// https://stackoverflow.com/q/3277182/1008999
var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0;

function bom(blob, opts) {
if (typeof opts === 'undefined') opts = {
autoBom: false
};else if (typeof opts !== 'object') {
console.warn('Deprecated: Expected third argument to be a object');
opts = {
autoBom: !opts
};
} // prepend BOM for UTF-8 XML and text/* types (including HTML)
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF

if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], {
type: blob.type
});
}

return blob;
}

function download(url, name, opts) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'blob';

xhr.onload = function () {
saveAs(xhr.response, name, opts);
};

xhr.onerror = function () {
console.error('could not download file');
};

xhr.send();
}

function corsEnabled(url) {
var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker

xhr.open('HEAD', url, false);

try {
xhr.send();
} catch (e) {}

return xhr.status >= 200 && xhr.status <= 299;
} // `a.click()` doesn't work for all browsers (#465)


function click(node) {
try {
node.dispatchEvent(new MouseEvent('click'));
} catch (e) {
var evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
node.dispatchEvent(evt);
}
}

var saveAs = _global.saveAs || ( // probably in some web worker
typeof window !== 'object' || window !== _global ? function saveAs() {}
/* noop */
// Use download attribute first if possible (#193 Lumia mobile)
: 'download' in HTMLAnchorElement.prototype ? function saveAs(blob, name, opts) {
var URL = _global.URL || _global.webkitURL;
var a = document.createElement('a');
name = name || blob.name || 'download';
a.download = name;
a.rel = 'noopener'; // tabnabbing
// TODO: detect chrome extensions & packaged apps
// a.target = '_blank'

if (typeof blob === 'string') {
// Support regular links
a.href = blob;

if (a.origin !== location.origin) {
corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank');
} else {
click(a);
}
} else {
// Support blobs
a.href = URL.createObjectURL(blob);
setTimeout(function () {
URL.revokeObjectURL(a.href);
}, 4E4); // 40s

setTimeout(function () {
click(a);
}, 0);
}
} // Use msSaveOrOpenBlob as a second approach
: 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) {
name = name || blob.name || 'download';

if (typeof blob === 'string') {
if (corsEnabled(blob)) {
download(blob, name, opts);
} else {
var a = document.createElement('a');
a.href = blob;
a.target = '_blank';
setTimeout(function () {
click(a);
});
}
} else {
navigator.msSaveOrOpenBlob(bom(blob, opts), name);
}
} // Fallback to using FileReader and a popup
: function saveAs(blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open('', '_blank');

if (popup) {
popup.document.title = popup.document.body.innerText = 'downloading...';
}

if (typeof blob === 'string') return download(blob, name, opts);
var force = blob.type === 'application/octet-stream';

var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;

var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);

if ((isChromeIOS || force && isSafari) && typeof FileReader === 'object') {
// Safari doesn't allow downloading of blob URLs
var reader = new FileReader();

reader.onloadend = function () {
var url = reader.result;
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;');
if (popup) popup.location.href = url;else location = url;
popup = null; // reverse-tabnabbing #460
};

reader.readAsDataURL(blob);
} else {
var URL = _global.URL || _global.webkitURL;
var url = URL.createObjectURL(blob);
if (popup) popup.location = url;else location.href = url;
popup = null; // reverse-tabnabbing #460

setTimeout(function () {
URL.revokeObjectURL(url);
}, 4E4); // 40s
}
});
_global.saveAs = saveAs.saveAs = saveAs;

if (typeof module !== 'undefined') {
module.exports = saveAs;
}
});

89 changes: 40 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,65 @@
[![Build Status](https://api.travis-ci.org/watsonbox/exportify.svg?branch=master)](https://travis-ci.org/watsonbox/exportify)
[![Build Status](http://img.shields.io/travis/watsonbox/exportify.svg?style=flat)](https://travis-ci.org/watsonbox/exportify)
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/watsonbox/exportify/master)

<a href="https://rawgit.com/watsonbox/exportify/master/exportify.html"><img src="screenshot.png"/></a>

Export your Spotify playlists using the Web API by clicking on the link below:

[https://watsonbox.github.io/exportify/](https://watsonbox.github.io/exportify/)

As many users have noted, there is no way to export/archive playlists from the Spotify client for safekeeping. This application provides a simple interface for doing that using the Spotify Web API.

No data will be saved - the entire application runs in the browser.


## Usage

Click 'Get Started', grant Exportify read-only access to your playlists, then click the 'Export' button to export a playlist.

Click 'Export All' to save a zip file containing a CSV file for each playlist in your account. This may take a while when many playlists exist and/or they are large.


### Re-importing Playlists

Once playlists are saved, it's also pretty straightforward to re-import them into Spotify. Open up the CSV file in Excel, for example, select and copy the `spotify:track:xxx` URIs, then simply create a playlist in Spotify and paste them in.
Export your Spotify playlists for analysis or just safekeeping: [exportify.net](https://exportify.net)

<a href="https://pavelkomarov.com/exportify/app"><img src="screenshot.png"/></a>

### Export Format

Track data is exported in [CSV](http://en.wikipedia.org/wiki/Comma-separated_values) format with the following fields:

- Spotify URI
- Spotify ID
- Artist IDs
- Track Name
- Artist Name
- Album Name
- Disc Number
- Track Number
- Track Duration (ms)
- Artist Name(s)
- Release Date
- Duration (ms)
- Popularity
- Added By
- Added At
- Genres
- Danceability
- Energy
- Key
- Loudness
- Mode
- Speechiness
- Acousticness
- Instrumentalness
- Liveness
- Valence
- Tempo
- Time Signature

### Analysis

## Development
Run the [Jupyter Notebook](https://github.com/watsonbox/exportify/blob/master/taste_analysis.ipynb) or [launch it in Binder](https://mybinder.org/v2/gh/watsonbox/exportify/master) to get a variety of plots about the music in a playlist including:

Developers wishing to make changes to Exportify should use a local web server. For example, using Python 2.7 (in the Exportify repo dir):
- Most common artists
- Most common genres
- Release date distribution
- Popularity distribution
- Comparisons of Acousticness, Valence, etc. to normal
- Time signatures and keys
- All songs plotted in 2D to indicate relative similarities

```bash
python -m SimpleHTTPServer
```

For Python 3 (in the Exportify repo dir):
### Development

Developers wishing to make changes to Exportify should use a local web server. For example, using Python (in the Exportify repo dir):

```bash
python -m http.server 8000
python -m http.server
```

Then open [http://localhost:8000](http://localhost:8000).

Then open [http://localhost:8000/exportify.html](http://localhost:8000/exportify.html).


## Notes

- The CSV export uses the HTML5 download attribute which is not [supported](http://caniuse.com/#feat=download) in all browsers. Where not supported the CSV will be rendered in the browser and must be saved manually.

- According to Spotify [documentation](https://developer.spotify.com/web-api/working-with-playlists/), "Folders are not returned through the Web API at the moment, nor can be created using it".

- It has been [pointed out](https://github.com/watsonbox/exportify/issues/6) that due to the large number of requests required to export all playlists, rate limiting errors may sometimes be encountered. Features will soon be added to make handling these more robust, but in the meantime these issues can be overcome by [creating your own Spotify application](https://github.com/watsonbox/exportify/issues/6#issuecomment-110793132).


## Contributing
### Contributing

1. Fork it ( https://github.com/watsonbox/exportify/fork )
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
3. Commit your changes (`git commit -m "message"`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
Loading