├── .editorconfig ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── app.dockerfile ├── karma.conf.js ├── package.json ├── src │ ├── actions │ │ ├── album.ts │ │ ├── artist.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── player.ts │ │ ├── track.ts │ │ └── user.ts │ ├── assets │ │ ├── backends │ │ │ └── jamendo.png │ │ └── logo_white.svg │ ├── components │ │ ├── Album.tsx │ │ ├── App.tsx │ │ ├── Backend.tsx │ │ ├── Collection.tsx │ │ ├── Header.tsx │ │ ├── Player.tsx │ │ ├── Router.tsx │ │ ├── Settings.tsx │ │ ├── User.tsx │ │ └── index.tsx │ ├── db.ts │ ├── interfaces │ │ ├── album.ts │ │ ├── artist.ts │ │ ├── backend.ts │ │ ├── doc.ts │ │ ├── index.ts │ │ ├── player.ts │ │ ├── router.ts │ │ ├── track.ts │ │ └── user.ts │ ├── main.d.ts │ ├── main.tsx │ ├── reducers │ │ ├── albums.ts │ │ ├── artists.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── player.ts │ │ ├── tracks.ts │ │ └── user.ts │ ├── store │ │ ├── createStore.ts │ │ └── pouchSync.ts │ └── stylesheets │ │ └── App.css ├── tests │ └── components │ │ ├── Album.tsx │ │ └── User.tsx ├── tsconfig.json ├── tsfmt.json ├── tslint.json └── webpack.config.js ├── doc ├── book.json └── src │ ├── README.md │ ├── SUMMARY.md │ ├── developer │ ├── app.md │ ├── developer.md │ └── lib.md │ ├── hosting │ ├── couchdb.md │ └── hosting.md │ └── user │ ├── backends.md │ ├── manager.md │ └── user.md ├── docker-compose.yml ├── lib ├── Cargo.lock ├── Cargo.toml ├── indexd.dockerfile ├── proxyd.dockerfile └── src │ ├── album.rs │ ├── artist.rs │ ├── bin │ ├── debug.rs │ ├── indexd.rs │ ├── manager.rs │ └── proxyd.rs │ ├── error.rs │ ├── index │ ├── file.rs │ ├── jamendo.rs │ ├── mod.rs │ └── webdav.rs │ ├── lib.rs │ ├── proxy │ ├── file.rs │ ├── jamendo.rs │ ├── mod.rs │ └── webdav.rs │ ├── track.rs │ ├── uri.rs │ ├── user.rs │ └── views.rs └── wercker.yml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.rs] 12 | indent_size = 4 13 | 14 | [*.md] 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | app/target 4 | app/node_modules 5 | app/typings 6 | 7 | lib/target 8 | 9 | doc/book 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Commit Message Format 2 | 3 | We follow the [Angular commit guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#-git-commit-guidelines). 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 The cloudfm contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cloudfm 2 | 3 | **cloudfm** is a music player that combines multiple music sources, like 4 | YouTube and Spotify, in a single app that works on any web browser or mobile 5 | phone, even offline. 6 | 7 | 8 | [![wercker status](https://app.wercker.com/status/a1c11952b1a5fc5d856e1ce7156d672e/s/master "wercker status")](https://app.wercker.com/project/byKey/a1c11952b1a5fc5d856e1ce7156d672e) 9 | 10 | * [Developer documentation](http://cloudfm.github.io/cloudfm/developer/developer.html) 11 | -------------------------------------------------------------------------------- /app/app.dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.10 2 | COPY . /src 3 | RUN apt-get update && apt-get install curl git -y && \ 4 | curl -sSf https://raw.githubusercontent.com/tj/n/master/bin/n | bash -s -- lts && \ 5 | cd /src && npm run init && npm run build && \ 6 | mv target/* /usr/share/nginx/html && rm -rf /src 7 | -------------------------------------------------------------------------------- /app/karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('./webpack.config.js'); 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | basePath: '', 6 | frameworks: ['mocha'], 7 | files: [ 8 | 'tests/**/*.ts', 9 | 'tests/**/*.tsx' 10 | ], 11 | exclude: [ 12 | ], 13 | preprocessors: { 14 | 'tests/**/*.ts': ['webpack'], 15 | 'tests/**/*.tsx': ['webpack'], 16 | }, 17 | webpack: Object.assign(webpackConfig, { 18 | entry: {}, 19 | externals: { 20 | 'jsdom': 'window', 21 | 'react/addons': true, 22 | 'react/lib/ExecutionEnvironment': true, 23 | 'react/lib/ReactContext': true 24 | } 25 | }), 26 | reporters: ['progress'], 27 | port: 9876, 28 | colors: true, 29 | logLevel: config.LOG_INFO, 30 | autoWatch: true, 31 | browsers: ['jsdom'], 32 | singleRun: false, 33 | concurrency: Infinity 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudfm-app", 3 | "version": "0.1.0", 4 | "description": "The cloud-aware music player.", 5 | "main": "index.js", 6 | "repository": "https://github.com/cloudfm/cloudfm.git", 7 | "scripts": { 8 | "test": "webpack && karma start --single-run", 9 | "build": "NODE_ENV=production webpack -p" 10 | }, 11 | "author": "Jakob Gillich ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "basscss": "~8.0.3", 15 | "basscss-background-colors": "~2.1.0", 16 | "basscss-base-reset": "~2.0.3", 17 | "basscss-base-typography": "~2.0.3", 18 | "basscss-border": "~4.0.2", 19 | "basscss-btn": "~1.1.1", 20 | "basscss-btn-outline": "~1.1.0", 21 | "basscss-btn-primary": "~1.1.0", 22 | "basscss-color-base": "~2.0.2", 23 | "basscss-color-tables": "~1.0.4", 24 | "basscss-colors": "~2.2.0", 25 | "basscss-defaults": "~2.1.3", 26 | "basscss-forms": "~1.0.0", 27 | "basscss-input-range": "~3.0.0", 28 | "basscss-progress": "~3.0.0", 29 | "font-awesome": "~4.6.3", 30 | "howler": "^2.0.1", 31 | "node-uuid": "~1.4.7", 32 | "pouchdb": "~6.0.7", 33 | "pouchdb-authentication": "~0.5.5", 34 | "react": "~15.3.2", 35 | "react-addons-shallow-compare": "~15.3.2", 36 | "react-dom": "~15.3.2", 37 | "react-redux": "~4.4.5", 38 | "react-redux-form": "~1.0.12", 39 | "react-router": "~2.8.1", 40 | "react-router-redux": "~4.0.6", 41 | "react-virtualized": "~8.2.0", 42 | "redux": "~3.6.0", 43 | "redux-logger": "~2.7.0", 44 | "redux-reset": "~0.2.0", 45 | "redux-thunk": "~2.1.0" 46 | }, 47 | "devDependencies": { 48 | "@types/chai": "^3.4.34", 49 | "@types/core-decorators": "^0.10.30", 50 | "@types/core-js": "^0.9.34", 51 | "@types/enzyme": "^2.4.36", 52 | "@types/howler": "^1.1.20", 53 | "@types/mocha": "^2.2.32", 54 | "@types/node": "^6.0.45", 55 | "@types/pouchdb": "^5.4.28", 56 | "@types/react": "^0.14.41", 57 | "@types/react-dom": "^0.14.18", 58 | "@types/react-redux": "^4.4.32", 59 | "@types/react-router": "^2.0.38", 60 | "@types/react-router-redux": "^4.0.34", 61 | "@types/redux": "^3.6.31", 62 | "@types/redux-logger": "^2.6.32", 63 | "@types/redux-thunk": "^2.1.31", 64 | "autoprefixer": "~6.5.1", 65 | "chai": "~3.5.0", 66 | "core-decorators": "~0.13.0", 67 | "core-js": "~2.4.1", 68 | "css-loader": "~0.25.0", 69 | "dotenv": "~2.0.0", 70 | "enzyme": "^2.5.1", 71 | "file-loader": "~0.9.0", 72 | "html-webpack-plugin": "~2.24.0", 73 | "json-loader": "^0.5.4", 74 | "karma": "~1.3.0", 75 | "karma-chrome-launcher": "~2.0.0", 76 | "karma-firefox-launcher": "~1.0.0", 77 | "karma-jsdom-launcher": "~4.0.0", 78 | "karma-mocha": "~1.2.0", 79 | "karma-webpack": "~1.8.0", 80 | "mocha": "~3.1.2", 81 | "postcss-calc": "~5.3.1", 82 | "postcss-custom-media": "~5.0.1", 83 | "postcss-custom-properties": "~5.0.1", 84 | "postcss-discard-comments": "~2.0.4", 85 | "postcss-import": "~8.1.2", 86 | "postcss-loader": "~1.0.0", 87 | "postcss-remove-root": "0.0.2", 88 | "postcss-reporter": "~1.4.1", 89 | "react-addons-test-utils": "^15.3.2", 90 | "style-loader": "~0.13.1", 91 | "ts-loader": "~0.9.5", 92 | "tslint": "~3.15.1", 93 | "tslint-loader": "~2.1.5", 94 | "typescript": "~2.0.3", 95 | "webpack": "~1.13.2", 96 | "webpack-fail-plugin": "~1.0.5" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/actions/album.ts: -------------------------------------------------------------------------------- 1 | import {Action} from "./"; 2 | import {Album} from "../interfaces"; 3 | 4 | export interface AlbumAction { 5 | type: Action; 6 | album: Album; 7 | }; 8 | -------------------------------------------------------------------------------- /app/src/actions/artist.ts: -------------------------------------------------------------------------------- 1 | import {Action} from "./"; 2 | import {Artist} from "../interfaces"; 3 | 4 | export interface ArtistAction { 5 | type: Action; 6 | artist: Artist; 7 | }; 8 | -------------------------------------------------------------------------------- /app/src/actions/error.ts: -------------------------------------------------------------------------------- 1 | import {Action} from "./"; 2 | 3 | export interface ErrorAction { 4 | type: Action; 5 | error: string; 6 | }; 7 | -------------------------------------------------------------------------------- /app/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./track"; 2 | export * from "./player"; 3 | export * from "./user"; 4 | export * from "./artist"; 5 | export * from "./error"; 6 | export * from "./album"; 7 | 8 | // Workaround until we get proper string enums 9 | // https://github.com/Microsoft/TypeScript/issues/3192 10 | export type Action = 11 | "INSERT_ALBUM" | "UPDATE_ALBUM" | "REMOVE_ALBUM" | 12 | "INSERT_ARTIST" | "UPDATE_ARTIST" | "REMOVE_ARTIST" | 13 | "INSERT_TRACK" | "UPDATE_TRACK" | "REMOVE_TRACK" | 14 | "PLAY_ALBUM" | "PLAY_ARTIST" | "PLAY_TRACK" | 15 | "PAUSE_PLAYER" | "FORWARD_PLAYER" | "BACKWARD_PLAYER" | 16 | "LOGIN_USER" | "SIGNUP_USER" | "UPDATE_USER" | 17 | "ADD_ERROR" | "RESET_ERROR"; 18 | 19 | /* tslint:disable:object-literal-sort-keys */ 20 | export const Action = { 21 | InsertAlbum: "INSERT_ALBUM" as Action, 22 | RemoveAlbum: "UPDATE_ALBUM" as Action, 23 | UpdateAlbum: "REMOVE_ALBUM" as Action, 24 | 25 | InsertArtist: "INSERT_ARTIST" as Action, 26 | RemoveArtist: "UPDATE_ARTIST" as Action, 27 | UpdateArtist: "REMOVE_ARTIST" as Action, 28 | 29 | InsertTrack: "INSERT_TRACK" as Action, 30 | RemoveTrack: "UPDATE_TRACK" as Action, 31 | UpdateTrack: "REMOVE_TRACK" as Action, 32 | 33 | PlayAlbum: "PLAY_ALBUM" as Action, 34 | PlayArtist: "PLAY_ARTIST" as Action, 35 | PlayTrack: "PLAY_TRACK" as Action, 36 | 37 | ForwardPlayer: "FORWARD_PLAYER" as Action, 38 | BackwardPlayer: "BACKWARD_PLAYER" as Action, 39 | PausePlayer: "PAUSE_PLAYER" as Action, 40 | 41 | LoginUser: "LOGIN_USER" as Action, 42 | SignupUser: "SIGNUP_USER" as Action, 43 | UpdateUser: "UPDATE_USER" as Action, 44 | 45 | AddError: "ADD_ERROR" as Action, 46 | ResetError: "RESET_ERROR" as Action, 47 | }; 48 | -------------------------------------------------------------------------------- /app/src/actions/player.ts: -------------------------------------------------------------------------------- 1 | import {Action} from "./"; 2 | import {Track} from "../interfaces"; 3 | 4 | export interface PlayerAction { 5 | type: Action; 6 | track?: Track; 7 | }; 8 | 9 | export enum PlayerState { 10 | Playing, 11 | Paused 12 | }; 13 | 14 | export function playTrack(track: Track): PlayerAction { 15 | return { 16 | type: Action.PlayTrack, 17 | track, 18 | }; 19 | } 20 | 21 | export function forwardPlayer(): PlayerAction { 22 | return { 23 | type: Action.ForwardPlayer, 24 | }; 25 | }; 26 | 27 | export function backwardPlayer(): PlayerAction { 28 | return { 29 | type: Action.BackwardPlayer, 30 | }; 31 | }; 32 | 33 | export function pausePlayer(): PlayerAction { 34 | return { 35 | type: Action.PausePlayer, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /app/src/actions/track.ts: -------------------------------------------------------------------------------- 1 | import {Action} from "./"; 2 | import {Track} from "../interfaces"; 3 | 4 | export interface TrackAction { 5 | type: Action; 6 | track: Track; 7 | }; 8 | -------------------------------------------------------------------------------- /app/src/actions/user.ts: -------------------------------------------------------------------------------- 1 | import {Action} from "./"; 2 | import {Dispatch} from "redux"; 3 | import {getLocalDb, getRemoteDb, getUsersDb} from "../db"; 4 | import {User} from "../interfaces"; 5 | import {push} from "react-router-redux"; 6 | 7 | export interface UserAction { 8 | type: Action; 9 | user: User; 10 | }; 11 | 12 | export function getUser(user: User): (dispatch: Dispatch) => void { 13 | return function(dispatch: Dispatch): void { 14 | const remoteDb = getRemoteDb(user.name); 15 | 16 | remoteDb.getUser(user.name, (err, user) => { 17 | if(err) { 18 | console.error(err); 19 | return dispatch({error: err.error, type: Action.AddError}); 20 | } 21 | 22 | dispatch({type: Action.UpdateUser, user}); 23 | }); 24 | }; 25 | } 26 | 27 | export function updateUser(user: User): (dispatch: Dispatch) => void { 28 | return function(dispatch: Dispatch): void { 29 | const remoteDb = getRemoteDb(user.name); 30 | const metadata = {backends: user.backends, email: user.email}; 31 | 32 | remoteDb.putUser(user.name, {metadata: metadata}, 33 | (err, response) => { 34 | if(err) { 35 | console.error(err); 36 | return dispatch({error: err.error, type: Action.AddError}); 37 | } 38 | 39 | dispatch({type: Action.UpdateUser, user}); 40 | }); 41 | }; 42 | } 43 | 44 | export function resumeSession(callback: (loggedIn: boolean) => void): 45 | (dispatch: Dispatch) => void { 46 | return function(dispatch: Dispatch): void { 47 | const usersDb = getUsersDb(); 48 | 49 | usersDb.getSession((err, response) => { 50 | if(!err && response.userCtx.name) { 51 | const remoteDb = getRemoteDb(response.userCtx.name); 52 | const localDb = getLocalDb(response.userCtx.name); 53 | localDb.sync(remoteDb, {live: true, retry: true}) 54 | .on("error", console.error.bind(console)); 55 | 56 | usersDb.getUser(response.userCtx.name, (err, user) => { 57 | if(err) { 58 | console.error(err); 59 | callback(false); 60 | return dispatch({error: err.name, type: Action.AddError}); 61 | } 62 | 63 | dispatch({type: Action.LoginUser, user: user}); 64 | callback(true); 65 | }); 66 | } else { 67 | callback(false); 68 | } 69 | }); 70 | }; 71 | } 72 | 73 | export function loginUser(user: User, redirectTo: string): 74 | (dispatch: Dispatch) => void { 75 | return function(dispatch: Dispatch): void { 76 | const remoteDb = getRemoteDb(user.name); 77 | 78 | remoteDb.login(user.name, user.password, (err, response) => { 79 | if(err) { 80 | console.error(err); 81 | return dispatch({error: err.error, type: Action.LoginUser}); 82 | } 83 | 84 | remoteDb.getUser(user.name, (err, user) => { 85 | if(err) { 86 | console.error(err); 87 | return dispatch({error: err.error, type: Action.AddError}); 88 | } 89 | 90 | const localDb = getLocalDb(user.name); 91 | localDb.sync(remoteDb, {live: true, retry: true}) 92 | .on("error", console.error.bind(console)); 93 | 94 | dispatch({type: Action.LoginUser, user: user}); 95 | dispatch(push(redirectTo)); 96 | }); 97 | }); 98 | }; 99 | } 100 | 101 | export function signupUser(user: User, redirectTo: string): 102 | (dispatch: Dispatch) => void { 103 | return function(dispatch: Dispatch): void { 104 | const usersDb = getUsersDb(); 105 | usersDb.signup(user.name, user.password, 106 | {metadata: {backends: [], email: user.email}}, 107 | (err, response) => { 108 | if(err) { 109 | console.error(err); 110 | return dispatch({error: err, type: Action.AddError}); 111 | } 112 | 113 | dispatch({type: Action.SignupUser, user}); 114 | dispatch(loginUser(user, redirectTo)); 115 | }); 116 | }; 117 | } 118 | 119 | -------------------------------------------------------------------------------- /app/src/assets/backends/jamendo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jgillich/cloudfm/3e543aa935c0e69ee0d58dad18dcacf032cd3006/app/src/assets/backends/jamendo.png -------------------------------------------------------------------------------- /app/src/assets/logo_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /app/src/components/Album.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {StatelessComponent} from "react"; 3 | import {connect} from "react-redux"; 4 | import {Album, Artist, Track, RouterProps} from "../interfaces"; 5 | import {Link} from "react-router"; 6 | import {playTrack} from "../actions"; 7 | 8 | interface AlbumListProps { 9 | albums: Album[]; 10 | } 11 | 12 | export const AlbumList: StatelessComponent = ({albums}) => { 13 | return ( 14 |
15 |
16 | {albums.map(a => ( 17 | 19 | 20 |
{a.name}
21 |
{a.artist}
22 | 23 | ))} 24 |
25 |
26 | ); 27 | }; 28 | 29 | export const AlbumListContainer = connect( 30 | (state, ownProps: RouterProps) => { 31 | let id = ownProps.params.id; 32 | let albums; 33 | 34 | if(id) { 35 | let artist = state.artists.find(a => a._id === id); 36 | 37 | if(!artist) { 38 | throw new Error("invalid artist id: " + id); 39 | } 40 | 41 | albums = state.albums.filter(a => a.artist === id); 42 | } else { 43 | albums = state.albums; 44 | } 45 | 46 | return { 47 | albums: albums.map(a => { 48 | let artist = state.artists.find(ar => ar._id === a.artist).name; 49 | return { 50 | _id: a._id, 51 | artist: artist, 52 | name: a.name, 53 | }; 54 | }), 55 | }; 56 | } 57 | )(AlbumList); 58 | 59 | interface AlbumItemProps { 60 | handleClick: (track: Track) => void; 61 | album: Album; 62 | artist: Artist; 63 | tracks: Track[]; 64 | } 65 | 66 | export const AlbumItem: StatelessComponent = 67 | ({album, artist, tracks, handleClick}) => { 68 | return ( 69 |
70 |
71 | 72 |
{album.name}
73 |
{artist.name}
74 |
75 | 84 |
85 | ); 86 | }; 87 | 88 | export const AlbumItemContainer = connect( 89 | (state, ownProps: RouterProps) => { 90 | let album = state.albums.find(a => a._id === ownProps.params.id); 91 | if(!album) { 92 | throw new Error("invalid album id"); 93 | }; 94 | 95 | let artist = state.artists 96 | .find(a => a._id === album.artist); 97 | 98 | let tracks = state.tracks 99 | .filter(t => t.album === album._id) 100 | .sort((a, b) => a.number - b.number); 101 | 102 | return { 103 | album, 104 | artist, 105 | tracks, 106 | }; 107 | }, 108 | (dispatch) => ({ 109 | handleClick: (track: Track): void => dispatch(playTrack(track)), 110 | }) 111 | )(AlbumItem); 112 | -------------------------------------------------------------------------------- /app/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Component} from "react"; 3 | import {StatelessComponent} from "react"; 4 | import {connect} from "react-redux"; 5 | import {Header, PlayerContainer} from "../components"; 6 | import {User} from "../interfaces"; 7 | require("../stylesheets/App.css"); 8 | 9 | interface AppProps { 10 | children: Component; 11 | user: User; 12 | }; 13 | 14 | export const App: StatelessComponent = ({children, user}) => ( 15 |
17 | {user.loggedIn ?
: null} 18 | {children} 19 | {user.loggedIn ? : null} 20 |
21 | ); 22 | 23 | export const AppContainer = connect( 24 | ({user}) => ({user}) 25 | )(App); 26 | -------------------------------------------------------------------------------- /app/src/components/Backend.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Component, ReactElement} from "react"; 3 | import {Dispatch} from "redux"; 4 | import {connect} from "react-redux"; 5 | import {updateUser} from "../actions"; 6 | import {User, Backend, isJamendoBackend, isFileBackend} from "../interfaces"; 7 | import { Field, Form } from "react-redux-form"; 8 | const jamendoIcon = require("../assets/backends/jamendo.png"); 9 | 10 | interface BackendSettingsProps { 11 | user: User; 12 | dispatch: Dispatch; 13 | addBackend: Backend; 14 | } 15 | 16 | class BackendSettings extends Component { 17 | 18 | public props: BackendSettingsProps; 19 | 20 | private handleSubmit(backend: Backend): void { 21 | let { dispatch, user } = this.props; 22 | user.backends.push(backend); 23 | dispatch(updateUser(user)); 24 | } 25 | 26 | public render(): ReactElement { 27 | let { user, addBackend } = this.props; 28 | 29 | return ( 30 |
31 |
All Backends
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | { user.backends.length ? user.backends.map(b => { 40 | if(isJamendoBackend(b)) { 41 | return ( 42 | 43 | 44 | 45 | 46 | ); 47 | } else if(isFileBackend(b)) { 48 | return ( 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | }) : } 56 | 57 |
Type Name
Jamendo{b.user_name}
File{b.machine_id}
No backends found
58 | 59 |
Add Backend
60 | 61 |
62 | 69 | 76 | 83 | 90 |
91 |
92 |
this.handleSubmit(backend) }> 94 |
95 | {(() => { 96 | switch(addBackend.type) { 97 | case "jamendo": 98 | return ( 99 |
100 | 101 | 103 | 104 |
105 | ); 106 | case "file": 107 | return ( 108 |
109 | 110 | 112 | 113 | 114 | 116 | 117 |
118 | ); 119 | default: 120 | throw new Error("unknown backend type: " + addBackend.type); 121 | } 122 | })()} 123 | 124 | 127 |
128 |
129 | 130 |
131 | ); 132 | } 133 | }; 134 | 135 | export const BackendSettingsContainer = connect( 136 | (state) => ({addBackend: state.addBackend, user: state.user}), 137 | (dispatch) => ({dispatch}) 138 | )(BackendSettings); 139 | -------------------------------------------------------------------------------- /app/src/components/Collection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {StatelessComponent, ReactElement} from "react"; 3 | import {connect} from "react-redux"; 4 | import {Link} from "react-router"; 5 | import {playTrack} from "../actions"; 6 | import {Artist, Album} from "../interfaces"; 7 | 8 | interface CollectionSidebarProps { 9 | artists: Artist[]; 10 | }; 11 | 12 | export const CollectionSidebar: StatelessComponent = 13 | ({artists}) => ( 14 |
15 | {artists.map(a => ( 16 |
17 | 19 | {a.name} 20 | 21 |
22 | ))} 23 |
24 | ); 25 | 26 | interface CollectionProps { 27 | artists: Artist[]; 28 | onTrackClick: () => void; 29 | children: ReactElement; 30 | active: string; 31 | }; 32 | 33 | function artistsWithAlbums(artists: Artist[], albums: Album[]): Artist[] { 34 | return artists.filter(a => !!albums.filter(al => al.artist === a._id).length); 35 | } 36 | 37 | export const Collection: StatelessComponent = 38 | ({children, artists, active}) => ( 39 |
40 | 41 | {children} 42 |
43 | ); 44 | 45 | export const CollectionContainer = connect( 46 | (state) => ({ 47 | artists: artistsWithAlbums(state.artists, state.albums), 48 | }), 49 | dispatch => ({ 50 | onTrackClick: (track): void => dispatch(playTrack(track)), 51 | }) 52 | )(Collection); 53 | -------------------------------------------------------------------------------- /app/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {StatelessComponent} from "react"; 3 | import {Link} from "react-router"; 4 | const logo = require("../assets/logo_white.svg"); 5 | 6 | export const Header: StatelessComponent<{}> = () => ( 7 | 26 | ); 27 | -------------------------------------------------------------------------------- /app/src/components/Player.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Component, ReactElement} from "react"; 3 | import {Dispatch} from "redux"; 4 | import {connect} from "react-redux"; 5 | import {Link} from "react-router"; 6 | import {Howl} from "howler"; 7 | import {debounce} from "core-decorators"; 8 | import {Track, Artist} from "../interfaces"; 9 | import {playTrack, pausePlayer, forwardPlayer, backwardPlayer, 10 | } from "../actions"; 11 | 12 | interface PlayerProps { 13 | track: Track; 14 | artist: Artist; 15 | playing: boolean; 16 | dispatch: Dispatch; 17 | } 18 | 19 | interface PlayerState { 20 | // The actual progress of the played track 21 | trackProgress?: number; 22 | // The displayed progress of the played track. This might differ from 23 | // trackProgress when the user manually moves the progress slider 24 | displayProgress?: number | any; // FIXME 25 | } 26 | 27 | export class Player extends Component { 28 | 29 | // Is set to true when the user moves the progress slider 30 | private disableProgressUpdate: boolean; 31 | 32 | private howl: any; // FIXME Howl 33 | 34 | public constructor(props: PlayerProps) { 35 | super(); 36 | this.state = {displayProgress: 0, trackProgress: 0}; 37 | } 38 | 39 | public initHowl({playing, track}: PlayerProps = this.props): void { 40 | if(this.howl) { 41 | this.howl.unload(); 42 | } 43 | 44 | let timerId = setInterval(() => { 45 | this.setState({ 46 | trackProgress: (this.howl.seek() / this.howl.duration()) * 100, 47 | }); 48 | }, 500); 49 | 50 | this.howl = new (Howl as any)({ 51 | autoplay: playing, 52 | onend: (): void => { 53 | clearInterval(timerId); 54 | if(this.props.track === track) { 55 | this.props.dispatch(forwardPlayer()); 56 | } 57 | }, 58 | src: `${process.env.SERVER_URL}/tracks/${track.uris[0]}.mp3`, 59 | }); 60 | } 61 | 62 | public componentWillUpdate(nextProps: PlayerProps, nextState: void): void { 63 | if(!this.props.track || nextProps.track._id !== this.props.track._id) { 64 | this.initHowl(nextProps); 65 | } else if(nextProps.playing && !this.props.playing) { 66 | this.howl.play(); 67 | } else if(!nextProps.playing && this.props.playing) { 68 | this.howl.pause(); 69 | } 70 | } 71 | 72 | public componentWillUnmount(): void { 73 | if(this.howl) { 74 | this.howl.unload(); 75 | } 76 | } 77 | 78 | public render(): ReactElement { 79 | let {track, artist, playing, dispatch} = this.props; 80 | let {displayProgress, trackProgress} = this.state; 81 | 82 | if(!this.disableProgressUpdate && displayProgress !== trackProgress) { 83 | displayProgress = trackProgress; 84 | } 85 | 86 | return ( 87 |
88 | 113 |
114 | {track ? 115 |
116 |
118 | 119 | 120 | {track.name} 121 | 122 | - 123 | 124 | {artist.name} 125 | 126 | 127 |
128 | { 129 | this.disableProgressUpdate = true; 130 | let value = parseInt((e.target as any).value, 10); 131 | this.setState({displayProgress: value}); 132 | this.seek(value); 133 | }}/> 134 |
135 | : null} 136 |
137 | 145 |
146 | ); 147 | } 148 | 149 | @debounce(500) 150 | private seek(percent: number): void { 151 | this.howl.seek(this.howl.duration() * (percent / 100)); 152 | this.disableProgressUpdate = false; 153 | } 154 | } 155 | 156 | export const PlayerContainer = connect( 157 | (state) => { 158 | let track = state.player.track; 159 | let artist; 160 | 161 | if(track) { 162 | artist = state.artists.find(a => a._id === track.artist); 163 | } 164 | 165 | return { 166 | artist: artist, 167 | playing: state.player.playing, 168 | track: track, 169 | }; 170 | }, 171 | (dispatch) => ({dispatch}) 172 | )(Player); 173 | -------------------------------------------------------------------------------- /app/src/components/Router.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Store} from "redux"; 3 | import {StatelessComponent} from "react"; 4 | import { 5 | AppContainer, 6 | LoginContainer, SignupContainer, 7 | CollectionContainer, AlbumListContainer, AlbumItemContainer, 8 | SettingsContainer, UserSettingsContainer, BackendSettingsContainer, 9 | } from "../components"; 10 | import { 11 | Router as ReactRouter, IndexRoute, IndexRedirect, Route, RouterState, 12 | RedirectFunction, 13 | } from "react-router"; 14 | import {ReactRouterReduxHistory} from "react-router-redux"; 15 | import {resumeSession} from "../actions"; 16 | 17 | function requireAuth(store: Store): 18 | (nextState: RouterState, replace: RedirectFunction, cb: Function) => void { 19 | return (nextState, replace, cb) => { 20 | let { user } = store.getState(); 21 | 22 | if(!user.loggedIn) { 23 | store.dispatch(resumeSession((loggedIn) => { 24 | if(loggedIn) { 25 | cb(); 26 | } else { 27 | replace({ 28 | pathname: "/login", 29 | query: { redirectTo: nextState.location.pathname }, 30 | }); 31 | cb(); 32 | } 33 | })); 34 | } else { 35 | cb(); 36 | } 37 | }; 38 | } 39 | 40 | interface RouterProps { 41 | history: ReactRouterReduxHistory; 42 | store: Store; 43 | }; 44 | 45 | export const Router: StatelessComponent = ({history, store}) => ( 46 | 47 | 48 | 49 | 50 | 51 | 53 | 54 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | -------------------------------------------------------------------------------- /app/src/components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {StatelessComponent, Component} from "react"; 3 | import {Link} from "react-router"; 4 | import {connect} from "react-redux"; 5 | 6 | interface SettingsSidebarProps { 7 | }; 8 | 9 | export const SettingsSidebar: StatelessComponent = 10 | () => ( 11 |
12 |
13 | 14 | User 15 | 16 |
17 |
18 | 19 | Backends 20 | 21 |
22 |
23 | ); 24 | 25 | interface SettingsProps { 26 | children: Component; 27 | active: string; 28 | }; 29 | 30 | export const Settings: StatelessComponent = 31 | ({children, active}) => ( 32 |
33 | 34 |
35 | {children} 36 |
37 |
38 | ); 39 | 40 | export const SettingsContainer = connect()(Settings); 41 | -------------------------------------------------------------------------------- /app/src/components/User.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Component, ReactElement} from "react"; 3 | import {Link} from "react-router"; 4 | import {connect} from "react-redux"; 5 | import {Dispatch, Store} from "redux"; 6 | import {User, RouterProps} from "../interfaces"; 7 | import {loginUser, signupUser, getUser} from "../actions"; 8 | import {Field, Form} from "react-redux-form"; 9 | const logo = require("../assets/logo_white.svg"); 10 | 11 | interface LoginProps { 12 | user: User; 13 | dispatch: Dispatch; 14 | redirectTo: string; 15 | } 16 | 17 | export class Login extends Component { 18 | 19 | public render(): ReactElement { 20 | let {dispatch, user, redirectTo} = this.props; 21 | 22 | return ( 23 |
24 |
25 |
dispatch(loginUser(user, redirectTo))}> 27 | 28 | 30 | 31 | 32 | 34 | 35 | 38 |
39 |
Sign up
40 |
41 | ); 42 | } 43 | }; 44 | 45 | export const LoginContainer = connect( 46 | (state, ownProps: RouterProps) => ({ 47 | redirectTo: (ownProps.location as any).query.redirectTo || "/", 48 | user: state.user, 49 | }), 50 | (dispatch) => ({dispatch}) 51 | )(Login); 52 | 53 | interface SignupProps { 54 | dispatch: Dispatch; 55 | redirectTo: string; 56 | } 57 | 58 | export class Signup extends Component { 59 | 60 | public render(): ReactElement { 61 | let {dispatch, redirectTo} = this.props; 62 | 63 | return ( 64 |
65 |
66 |
dispatch(signupUser(user, redirectTo))}> 68 | 69 | 71 | 72 | 73 | 75 | 76 | 77 | 79 | 80 | 83 |
84 |
Log in
85 |
86 | ); 87 | } 88 | }; 89 | 90 | export const SignupContainer = connect( 91 | (state, ownProps: RouterProps) => ({ 92 | redirectTo: (ownProps.location as any).query.redirectTo || "/", 93 | }), 94 | (dispatch) => ({dispatch}) 95 | )(Signup); 96 | 97 | interface UserSettingsProps { 98 | user: User; 99 | getUser: (user: User) => void; 100 | } 101 | 102 | export class UserSettings extends Component { 103 | 104 | public props: UserSettingsProps; 105 | 106 | public constructor(props: UserSettingsProps) { 107 | super(props); 108 | 109 | // When the user settings are loaded, we fetch the user once to make sure 110 | // our local data is up to date. 111 | this.props.getUser(this.props.user); 112 | } 113 | 114 | public render(): ReactElement { 115 | let {user} = this.props; 116 | 117 | return ( 118 |
119 | Logged in as {user.name} 120 |
121 | ); 122 | } 123 | }; 124 | 125 | export const UserSettingsContainer = connect( 126 | (state) => ({user: state.user}), 127 | (dispatch) => ({getUser: (user: User): void => dispatch(getUser(user))}) 128 | )(UserSettings); 129 | -------------------------------------------------------------------------------- /app/src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Album"; 2 | export * from "./App"; 3 | export * from "./Backend"; 4 | export * from "./Collection"; 5 | export * from "./Header"; 6 | export * from "./Player"; 7 | export * from "./Router"; 8 | export * from "./Settings"; 9 | export * from "./User"; 10 | -------------------------------------------------------------------------------- /app/src/db.ts: -------------------------------------------------------------------------------- 1 | import * as PouchDB from "pouchdb"; 2 | 3 | PouchDB.plugin(require("pouchdb-authentication")); 4 | 5 | let instances = {}; 6 | 7 | export function getLocalDb(username: string): PouchDB { 8 | let dbUrl = toDbName(username); 9 | if(!instances[dbUrl]) { 10 | instances[dbUrl] = new PouchDB(dbUrl); 11 | } 12 | return instances[dbUrl]; 13 | } 14 | 15 | export function getRemoteDb(username: string): PouchDB { 16 | let dbUrl = `${process.env.DATABASE_URL}/${toDbName(username)}`; 17 | if(!instances[dbUrl]) { 18 | instances[dbUrl] = new PouchDB(dbUrl); 19 | } 20 | return instances[dbUrl]; 21 | } 22 | 23 | export function getUsersDb(): PouchDB { 24 | let dbUrl = `${process.env.DATABASE_URL}/_users`; 25 | if(!instances[dbUrl]) { 26 | instances[dbUrl] = new PouchDB(dbUrl); 27 | } 28 | return instances[dbUrl]; 29 | } 30 | 31 | function toDbName(username: string): string { 32 | return "userdb-" + toHex(username); 33 | } 34 | 35 | function toHex(str: string): string { 36 | let result = ""; 37 | for(let i = 0; i < str.length; i++) { 38 | result += str.charCodeAt(i).toString(16); 39 | } 40 | return result; 41 | } 42 | -------------------------------------------------------------------------------- /app/src/interfaces/album.ts: -------------------------------------------------------------------------------- 1 | import {Doc} from "./"; 2 | 3 | export interface Album extends Doc { 4 | artist: string; 5 | name: string; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/interfaces/artist.ts: -------------------------------------------------------------------------------- 1 | import {Doc} from "./"; 2 | 3 | export interface Artist extends Doc { 4 | name: string; 5 | } 6 | -------------------------------------------------------------------------------- /app/src/interfaces/backend.ts: -------------------------------------------------------------------------------- 1 | import {Doc} from "./"; 2 | 3 | export type Backend = FileBackend | JamendoBackend; 4 | 5 | export interface FileBackend extends Doc { 6 | machine_id: string; 7 | paths: string[]; 8 | } 9 | 10 | export function isFileBackend(backend: Backend): backend is FileBackend { 11 | return backend.type === "file"; 12 | } 13 | 14 | export interface JamendoBackend extends Doc { 15 | user_name: string; 16 | } 17 | 18 | export function isJamendoBackend(backend: Backend): backend is JamendoBackend { 19 | return backend.type === "jamendo"; 20 | } 21 | -------------------------------------------------------------------------------- /app/src/interfaces/doc.ts: -------------------------------------------------------------------------------- 1 | export interface Doc { 2 | _id?: string; 3 | type?: string; 4 | } 5 | -------------------------------------------------------------------------------- /app/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./backend"; 2 | export * from "./doc"; 3 | export * from "./track"; 4 | export * from "./artist"; 5 | export * from "./user"; 6 | export * from "./backend"; 7 | export * from "./player"; 8 | export * from "./album"; 9 | export * from "./router"; 10 | -------------------------------------------------------------------------------- /app/src/interfaces/player.ts: -------------------------------------------------------------------------------- 1 | import {Track} from "../interfaces"; 2 | 3 | export interface PlayerState { 4 | playing: boolean; 5 | track?: Track; 6 | trackIndex?: number; 7 | playlist: Track[]; 8 | }; 9 | -------------------------------------------------------------------------------- /app/src/interfaces/router.ts: -------------------------------------------------------------------------------- 1 | export interface RouterProps { 2 | location: Location; 3 | params: RouterParams; 4 | } 5 | 6 | export interface RouterParams { 7 | id: string; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/interfaces/track.ts: -------------------------------------------------------------------------------- 1 | import {Doc} from "./"; 2 | 3 | export interface Track extends Doc { 4 | album: string; 5 | artist: string; 6 | name: string; 7 | number: number; 8 | uris: string[]; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/interfaces/user.ts: -------------------------------------------------------------------------------- 1 | import {Doc, Backend} from "./"; 2 | 3 | export interface User extends Doc { 4 | name?: string; 5 | password?: string; 6 | email?: string; 7 | backends?: Backend[]; 8 | loggedIn: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main.d.ts: -------------------------------------------------------------------------------- 1 | // TODO write proper typings and contribute them to dt 2 | 3 | interface ReactReduxForm { 4 | modelReducer(a: any, b?: any): any; 5 | formReducer(a: any, b?: any): any; 6 | Field(a: any): any; 7 | Form(a: any): any; 8 | actions(a: any): any; 9 | modeled(a: any, b: any): any; 10 | } 11 | 12 | declare var ReactReduxForm: ReactReduxForm; 13 | 14 | declare module "react-redux-form" { 15 | export = ReactReduxForm; 16 | } 17 | 18 | interface ReactHowler { 19 | (a: any, b?: any): any; 20 | } 21 | 22 | declare var ReactHowler: ReactHowler; 23 | 24 | declare module "react-howler" { 25 | export = ReactHowler; 26 | } 27 | 28 | // err should be PouchDB.Core.Error, but it's not properly defined yet 29 | interface PouchDB { 30 | plugin(plugin: any): void; 31 | sync(pouch: PouchDB, opts: any): any; 32 | getUser(name: string, opts?: any, callback?: (err: any, res: any) => void): any; 33 | putUser(name: string, opts?: any, callback?: (err: any, res: any) => void): any; 34 | login(name: string, password: string, opts?: any, callback?: (err: any, res: any) => void): any; 35 | signup(name: string, password: string, opts?: any, callback?: (err: any, res: any) => void): any; 36 | getSession(callback: (err: any, res: any) => void): any; 37 | } 38 | 39 | interface ReduxResetOptions { 40 | type?: string; 41 | data?: string; 42 | } 43 | 44 | declare module "redux-reset" { 45 | function reduxReset(options?: ReduxResetOptions): any; 46 | export default reduxReset; 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | require("core-js"); 4 | 5 | import * as React from "react"; 6 | import {render} from "react-dom"; 7 | import {browserHistory} from "react-router"; 8 | import {syncHistoryWithStore} from "react-router-redux"; 9 | import {Provider} from "react-redux"; 10 | import {Router} from "./components"; 11 | import createStore from "./store/createStore"; 12 | 13 | const store = createStore(); 14 | const history = syncHistoryWithStore(browserHistory, store); 15 | const container = document.createElement("div"); 16 | 17 | render( 18 | 19 | 20 | , 21 | container 22 | ); 23 | 24 | document.body.appendChild(container); 25 | -------------------------------------------------------------------------------- /app/src/reducers/albums.ts: -------------------------------------------------------------------------------- 1 | import {Album} from "../interfaces"; 2 | import {Action, AlbumAction} from "../actions"; 3 | 4 | function albumsReducer(state: Album[] = [], action: AlbumAction): Album[] { 5 | switch(action.type) { 6 | case Action.InsertAlbum: 7 | return [...state, action.album]; 8 | case Action.RemoveAlbum: 9 | return state.filter(t => t._id !== action.album._id); 10 | case Action.UpdateAlbum: 11 | return state.map(t => t._id === action.album._id ? action.album : t); 12 | default: 13 | return state; 14 | }; 15 | }; 16 | 17 | export default albumsReducer; 18 | -------------------------------------------------------------------------------- /app/src/reducers/artists.ts: -------------------------------------------------------------------------------- 1 | import {Artist} from "../interfaces"; 2 | import {Action, ArtistAction} from "../actions"; 3 | 4 | function artistsReducer(state: Artist[] = [], action: ArtistAction): Artist[] { 5 | switch(action.type) { 6 | case Action.InsertArtist: 7 | return [...state, action.artist]; 8 | case Action.RemoveArtist: 9 | return state.filter(t => t._id !== action.artist._id); 10 | case Action.UpdateArtist: 11 | return state.map(t => t._id === action.artist._id ? action.artist : t); 12 | default: 13 | return state; 14 | }; 15 | }; 16 | 17 | export default artistsReducer; 18 | -------------------------------------------------------------------------------- /app/src/reducers/errors.ts: -------------------------------------------------------------------------------- 1 | import {Action, ErrorAction} from "../actions"; 2 | 3 | const errorsReducer = (state = [], action: ErrorAction) => { 4 | const {type, error} = action; 5 | 6 | if(type === Action.ResetError) { 7 | return []; 8 | } else if(error) { 9 | state.push(action.error); 10 | } 11 | 12 | return state; 13 | }; 14 | 15 | export default errorsReducer; 16 | -------------------------------------------------------------------------------- /app/src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { routerReducer} from "react-router-redux"; 2 | import {modelReducer, formReducer, modeled} from "react-redux-form"; 3 | import { combineReducers} from "redux"; 4 | import albumsReducer from "./albums"; 5 | import artistsReducer from "./artists"; 6 | import tracksReducer from "./tracks"; 7 | import playerReducer from "./player"; 8 | import userReducer from "./user"; 9 | import errorsReducer from "./errors"; 10 | 11 | const rootReducer = combineReducers({ 12 | addBackend: modelReducer("addBackend", {type: "jamendo"}), 13 | addBackendForm: formReducer("addBackend", {}), 14 | albums: albumsReducer, 15 | artists: artistsReducer, 16 | errors: errorsReducer, 17 | player: playerReducer, 18 | routing: routerReducer, 19 | tracks: tracksReducer, 20 | user: modeled(userReducer, "user"), 21 | }); 22 | 23 | export default rootReducer; 24 | -------------------------------------------------------------------------------- /app/src/reducers/player.ts: -------------------------------------------------------------------------------- 1 | import {PlayerState} from "../interfaces"; 2 | import {Action, PlayerAction} from "../actions"; 3 | 4 | const playerReducer = 5 | (state: PlayerState = {playing: false, playlist: []}, action: PlayerAction) => { 6 | switch(action.type) { 7 | case Action.PlayTrack: 8 | return Object.assign({}, state, { 9 | playing: true, 10 | track: action.track, 11 | trackIndex: state.playlist.push(action.track) - 1, 12 | }); 13 | case Action.PausePlayer: 14 | return Object.assign({}, state, {playing: false}); 15 | case Action.ForwardPlayer: 16 | if(state.track) { 17 | let nextTrack = state.playlist[state.trackIndex + 1]; 18 | if(nextTrack) { 19 | return Object.assign({}, state, { 20 | track: nextTrack, 21 | trackIndex: state.trackIndex + 1, 22 | }); 23 | } 24 | } 25 | return state; 26 | case Action.BackwardPlayer: 27 | if(state.track) { 28 | let previousTrack = state.playlist[state.trackIndex - 1]; 29 | if(previousTrack) { 30 | return Object.assign({}, state, { 31 | track: previousTrack, 32 | trackIndex: state.trackIndex - 1, 33 | }); 34 | } 35 | } 36 | return state; 37 | default: 38 | return state; 39 | }; 40 | }; 41 | 42 | export default playerReducer; 43 | -------------------------------------------------------------------------------- /app/src/reducers/tracks.ts: -------------------------------------------------------------------------------- 1 | import {Track} from "../interfaces"; 2 | import {Action, TrackAction} from "../actions"; 3 | 4 | function tracksReducer(state: Track[] = [], action: TrackAction): Track[] { 5 | switch(action.type) { 6 | case Action.InsertTrack: 7 | return [...state, action.track]; 8 | case Action.RemoveTrack: 9 | return state.filter(t => t._id !== action.track._id); 10 | case Action.UpdateTrack: 11 | return state.map(t => t._id === action.track._id ? action.track : t); 12 | default: 13 | return state; 14 | }; 15 | }; 16 | 17 | export default tracksReducer; 18 | -------------------------------------------------------------------------------- /app/src/reducers/user.ts: -------------------------------------------------------------------------------- 1 | import {User} from "../interfaces"; 2 | import {Action, UserAction} from "../actions"; 3 | 4 | const userReducer = (state: User = {loggedIn: false}, action: UserAction) => { 5 | switch(action.type) { 6 | case Action.LoginUser: 7 | return Object.assign({}, state, action.user, {loggedIn: true}); 8 | case Action.SignupUser: 9 | return Object.assign({}, state, action.user); 10 | case Action.UpdateUser: 11 | return Object.assign({}, state, action.user); 12 | default: 13 | return state; 14 | }; 15 | }; 16 | 17 | export default userReducer; 18 | -------------------------------------------------------------------------------- /app/src/store/createStore.ts: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose } from "redux"; 2 | import {Store} from "redux"; 3 | import rootReducer from "../reducers"; 4 | import * as createLogger from "redux-logger"; 5 | import thunkMiddleware from "redux-thunk"; 6 | import {routerMiddleware } from "react-router-redux"; 7 | import {browserHistory } from "react-router"; 8 | import reduxReset from "redux-reset"; 9 | import pouchSync from "./pouchSync"; 10 | 11 | export default function (): Store { 12 | 13 | const applyMiddlewares = applyMiddleware( 14 | thunkMiddleware, 15 | routerMiddleware(browserHistory), 16 | pouchSync([ 17 | ["track", "tracks"], 18 | ["artist", "artists"], 19 | ["album", "albums"], 20 | ]), 21 | (createLogger)() 22 | ); 23 | 24 | const createStoreWithMiddleware = (compose as any)( 25 | applyMiddlewares, 26 | reduxReset({data: "state"}) 27 | )(createStore); 28 | 29 | return createStoreWithMiddleware( 30 | rootReducer 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /app/src/store/pouchSync.ts: -------------------------------------------------------------------------------- 1 | import {Store} from "redux"; 2 | import {getLocalDb} from "../db"; 3 | 4 | let listeners = []; 5 | 6 | // Types is a tuple with [singular, plural] 7 | export default function pouchSync(types: string[][]) { 8 | return (store) => { 9 | let {user} = store.getState(); 10 | 11 | return next => action => { 12 | let result = next(action); 13 | let state = store.getState(); 14 | if(state.user !== user) { 15 | user = state.user; 16 | if(listeners.length) { 17 | listeners.forEach(l => l.cancel()); 18 | } 19 | let db = getLocalDb(user.name); 20 | reinitializeStore(store, db, types); 21 | listeners = types.map(t => listen(store, db, t)); 22 | } 23 | return result; 24 | }; 25 | }; 26 | }; 27 | 28 | function listen(store: Store, db: any, type: string[]): void { 29 | return db.changes({ 30 | include_docs: true, 31 | live: true, 32 | since: "now", 33 | }).on("change", change => { 34 | if (change.doc.type !== type[0]) { 35 | return; 36 | } 37 | let actionType = change.deleted ? "DELETE" : 38 | store.getState()[type[1]].some(d => d._id === change.doc._id) 39 | ? "UPDATE" : "INSERT"; 40 | store.dispatch({ 41 | type: `${actionType}_${type[0].toUpperCase()}`, 42 | [type[0]]: change.doc, 43 | }); 44 | }); 45 | } 46 | 47 | // Replaces the current store with the new user data 48 | // When the store is initialized the first time, we don't know the username 49 | // because sign in has not happened yet. 50 | function reinitializeStore(store: Store, db: PouchDB, types: string[][]): void { 51 | let emptyState = {}; 52 | types.forEach(t => emptyState[t[1]] = []); 53 | 54 | let state = Object.assign(store.getState(), emptyState); 55 | 56 | (db as any).allDocs({ 57 | include_docs: true, 58 | }, (err, res) => { 59 | if(err) { 60 | return console.error(err.error); 61 | } 62 | 63 | res.rows.forEach(({doc}) => { 64 | let foundType = types.find(t => t[0] === doc.type); 65 | if(foundType) { 66 | state[foundType[1]].push(doc); 67 | } 68 | }); 69 | 70 | store.dispatch({ 71 | state: state, 72 | type: "RESET", 73 | }); 74 | }); 75 | 76 | } 77 | -------------------------------------------------------------------------------- /app/src/stylesheets/App.css: -------------------------------------------------------------------------------- 1 | @import "~font-awesome/css/font-awesome.css"; 2 | @import "basscss"; 3 | @import "basscss-base-reset"; 4 | @import "basscss-base-typography"; 5 | @import "basscss-btn"; 6 | @import "basscss-btn-outline"; 7 | @import "basscss-btn-primary"; 8 | @import "basscss-color-base"; 9 | @import "basscss-border"; 10 | @import "basscss-color-tables"; 11 | @import "basscss-colors"; 12 | @import "basscss-background-colors"; 13 | @import "basscss-forms"; 14 | @import "basscss-progress"; 15 | @import "basscss-input-range"; 16 | @import "basscss-defaults"; 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | .overflow-y-scroll { 23 | overflow-y: scroll; 24 | } 25 | 26 | a { 27 | cursor: pointer; 28 | outline: 0; 29 | } 30 | 31 | .btn:focus { 32 | border-color: transparent; 33 | box-shadow: none; 34 | } 35 | 36 | .inline-input { 37 | font-family: inherit; 38 | font-size: inherit; 39 | height: 2.5rem; 40 | padding: .5rem; 41 | margin-right: .5rem; 42 | border: 1px solid #ccc; 43 | border-radius: 3px; 44 | box-sizing: border-box; 45 | } 46 | 47 | .colum-count-2 { 48 | column-count: 2; 49 | } 50 | 51 | :root { 52 | 53 | --font-family: "Lato", "Helvetica Neue", Helvetica, sans-serif; 54 | --blue: #0099dd; 55 | } 56 | -------------------------------------------------------------------------------- /app/tests/components/Album.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {assert} from "chai"; 3 | import {AlbumList} from "../../src/components/Album"; 4 | import {render} from "enzyme"; 5 | 6 | describe("Album", function() { 7 | describe("", function () { 8 | it("renders two albums", () => { 9 | let albums = [ 10 | {_id: "foo", name: "Foo", artist: "Foo Fighters"}, 11 | {_id: "bar", name: "Bar", artist: "Bar Fighters"}, 12 | ]; 13 | const wrapper = render(); 14 | assert.lengthOf(wrapper.find("a"), 2); 15 | }); 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /app/tests/components/User.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {assert} from "chai"; 3 | import {AlbumList} from "../../src/components/Album"; 4 | import {render} from "enzyme"; 5 | 6 | describe("Album", function() { 7 | describe("", function () { 8 | it("renders two albums", () => { 9 | let albums = [ 10 | {_id: "foo", name: "Foo", artist: "Foo Fighters"}, 11 | {_id: "bar", name: "Bar", artist: "Bar Fighters"}, 12 | ]; 13 | const wrapper = render(); 14 | assert.lengthOf(wrapper.find("a"), 2); 15 | }); 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "sourceMap": true, 5 | "jsx": "react", 6 | "experimentalDecorators": true, 7 | "moduleResolution": "node", 8 | "types": [ 9 | "node", "mocha", "core-js" 10 | ] 11 | }, 12 | 13 | "compileOnSave": false 14 | } 15 | -------------------------------------------------------------------------------- /app/tsfmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "indentSize": 2, 3 | "tabSize": 2, 4 | "newLineCharacter": "\n", 5 | "convertTabsToSpaces": true, 6 | "insertSpaceAfterCommaDelimiter": true, 7 | "insertSpaceAfterSemicolonInForStatements": true, 8 | "insertSpaceBeforeAndAfterBinaryOperators": true, 9 | "insertSpaceAfterKeywordsInControlFlowStatements": false, 10 | "insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, 11 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 12 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, 13 | "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, 14 | "placeOpenBraceOnNewLineForFunctions": false, 15 | "placeOpenBraceOnNewLineForControlBlocks": false 16 | } 17 | -------------------------------------------------------------------------------- /app/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, "check-space"], 5 | "curly": true, 6 | "eofline": true, 7 | "indent": [true, "spaces"], 8 | "interface-name": [true, "never-prefix"], 9 | "max-line-length": [true, 100], 10 | "member-access": true, 11 | "no-angle-bracket-type-assertion": true, 12 | "no-any": true, 13 | "no-arg": true, 14 | "no-bitwise": true, 15 | "no-conditional-assignment": true, 16 | "no-consecutive-blank-lines": true, 17 | "no-console": [true, "log"], 18 | "no-construct": true, 19 | "no-eval": true, 20 | "no-invalid-this": true, 21 | "no-require-imports": false, 22 | "no-string-literal": true, 23 | "no-trailing-whitespace": true, 24 | "no-unreachable": true, 25 | "no-unused-expression": true, 26 | "no-unused-variable": [true, "react"], 27 | "no-use-before-declare": true, 28 | "no-var-keyword": true, 29 | "object-literal-sort-keys": true, 30 | "one-line": [true, 31 | "check-catch", 32 | "check-else", 33 | "check-finally", 34 | "check-open-brace", 35 | "check-whitespace" 36 | ], 37 | "quotemark": [true, "double", "jsx-double"], 38 | "radix": true, 39 | "semicolon": [true, "always"], 40 | "switch-default": true, 41 | "trailing-comma": [true, {"singleline": "never", "multiline": "always"}], 42 | "triple-equals": true, 43 | "typedef": [true, "call-signature", "parameter"], 44 | "use-strict": [true, "check-module"], 45 | "variable-name": [true, 46 | "check-format", 47 | "ban-keywords", 48 | "allow-pascal-case" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const webpack = require("webpack"); 3 | const path = require("path"); 4 | const failPlugin = require("webpack-fail-plugin"); 5 | const htmlPlugin = new (require("html-webpack-plugin"))({ 6 | title: "cloudfm", 7 | }); 8 | 9 | require("dotenv").config({path: "../.env", silent: true}); 10 | const envPlugin = new webpack.DefinePlugin({ 11 | "process.env": { 12 | "NODE_ENV": `"${process.env.NODE_ENV}"`, 13 | "DATABASE_URL": `"${process.env.DATABASE_URL || "http://localhost:5984"}"`, 14 | "SERVER_URL": `"${process.env.SERVER_URL || "http://localhost:8423"}"`, 15 | } 16 | }); 17 | 18 | module.exports = { 19 | devServer: { 20 | historyApiFallback: true, 21 | }, 22 | devtool: "#source-map", 23 | entry: "./src/main.tsx", 24 | module: { 25 | loaders: [ 26 | {loader: "file", test: /\.(jpe?g|png|gif)$/i}, 27 | {loader: "file", test: /\.(svg|woff2?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/i}, 28 | {loader: "ts-loader", test: /\.tsx?$/}, 29 | {loader: "style!css!postcss", test: /\.css$/}, 30 | {loader: 'json', test: /\.json$/ }, 31 | ], 32 | preLoaders: [ 33 | {loader: "tslint", test: /\.tsx?$/}, 34 | ], 35 | }, 36 | output: { 37 | filename: "bundle.js", 38 | path: path.resolve("target"), 39 | publicPath: "/", 40 | }, 41 | plugins: [failPlugin, htmlPlugin, envPlugin].concat(process.env.NODE_ENV === "production" ? [webpack.optimize.UglifyJsPlugin] : []), 42 | postcss: function () { 43 | return [ 44 | "autoprefixer", 45 | "postcss-import", 46 | "postcss-custom-media", 47 | "postcss-custom-properties", 48 | "postcss-calc", 49 | "postcss-discard-comments", 50 | "postcss-remove-root", 51 | "postcss-reporter", 52 | ].map(require); 53 | }, 54 | alias: { 55 | // FIXME doesn't work in TypeScript 56 | "app": path.join(__dirname, "./src/"), 57 | }, 58 | resolve: { 59 | extensions: ["", ".ts", ".tsx", ".js", ".css", ".json"], 60 | alias: { 61 | "basscss": "../node_modules/basscss/src/basscss.css", 62 | } 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /doc/book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "cloudfm Documentation", 3 | "description": "The cloud-aware music player", 4 | "author": "Jakob Gillich" 5 | } 6 | -------------------------------------------------------------------------------- /doc/src/README.md: -------------------------------------------------------------------------------- 1 | # cloudfm 2 | 3 | **cloudfm** is a music player that combines multiple music sources, like 4 | YouTube and Spotify, in a single app that works on any web browser or mobile 5 | phone, even offline. 6 | -------------------------------------------------------------------------------- /doc/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [cloudfm](README.md) 4 | - [User documentation](user/user.md) 5 | - [Backends](user/backends.md) 6 | - [Manager](user/manager.md) 7 | - [Hosting documentation](hosting/hosting.md) 8 | - [CouchDB](hosting/couchdb.md) 9 | - [Developer documentation](developer/developer.md) 10 | - [app](developer/app.md) 11 | - [lib](developer/lib.md) 12 | -------------------------------------------------------------------------------- /doc/src/developer/app.md: -------------------------------------------------------------------------------- 1 | # app 2 | -------------------------------------------------------------------------------- /doc/src/developer/developer.md: -------------------------------------------------------------------------------- 1 | # Developer documentation 2 | 3 | 4 | ## Quickstart 5 | 6 | * Install Node.js 4 or newer 7 | * Install Rust nightly 8 | 9 | $ curl -sSf https://static.rust-lang.org/rustup.sh | sh -s -- --channel=nightly 10 | 11 | * Start CouchDB with the couchperuser extension 12 | 13 | docker-compose up -d db 14 | 15 | * Enable CORS 16 | 17 | npm i -g add-cors-to-couchdb 18 | add-cors-to-couchdb 19 | 20 | * Start the app 21 | 22 | cd app 23 | npm i -g webpack webpack-dev-server 24 | npm run init 25 | webpack-dev-server 26 | 27 | * Start the indexing service 28 | 29 | cd lib 30 | cargo run --bin indexd 31 | 32 | Go to `http://localhost:8080`. 33 | -------------------------------------------------------------------------------- /doc/src/developer/lib.md: -------------------------------------------------------------------------------- 1 | # lib 2 | -------------------------------------------------------------------------------- /doc/src/hosting/couchdb.md: -------------------------------------------------------------------------------- 1 | # CouchDB 2 | -------------------------------------------------------------------------------- /doc/src/hosting/hosting.md: -------------------------------------------------------------------------------- 1 | # Hosting documentation 2 | -------------------------------------------------------------------------------- /doc/src/user/backends.md: -------------------------------------------------------------------------------- 1 | # Backends 2 | -------------------------------------------------------------------------------- /doc/src/user/manager.md: -------------------------------------------------------------------------------- 1 | # Manager 2 | -------------------------------------------------------------------------------- /doc/src/user/user.md: -------------------------------------------------------------------------------- 1 | # User documentation 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | db: 2 | image: klaemo/couchdb:1.6-couchperuser 3 | ports: 4 | - "5984:5984" 5 | -------------------------------------------------------------------------------- /lib/Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "cloudfm" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "chill 0.1.1+master (git+https://github.com/jgillich/chill.git)", 6 | "dotenv 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 7 | "hex 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 8 | "hyper 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", 9 | "hyperdav 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 10 | "id3 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 11 | "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 12 | "jamendo 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 13 | "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 14 | "logger 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 15 | "router 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 16 | "serde 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 17 | "serde_json 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 18 | "serde_macros 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 19 | "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 20 | "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 21 | "walkdir 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 22 | ] 23 | 24 | [[package]] 25 | name = "aho-corasick" 26 | version = "0.5.2" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | dependencies = [ 29 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 30 | ] 31 | 32 | [[package]] 33 | name = "aster" 34 | version = "0.17.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | 37 | [[package]] 38 | name = "base64" 39 | version = "0.1.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | 42 | [[package]] 43 | name = "bitflags" 44 | version = "0.7.0" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | 47 | [[package]] 48 | name = "byteorder" 49 | version = "0.5.3" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | 52 | [[package]] 53 | name = "chill" 54 | version = "0.1.1+master" 55 | source = "git+https://github.com/jgillich/chill.git#93503a77031f88511f1a1b2f168b8545595539b2" 56 | dependencies = [ 57 | "base64 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 58 | "hyper 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 59 | "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 60 | "regex 0.1.71 (registry+https://github.com/rust-lang/crates.io-index)", 61 | "serde 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 62 | "serde_json 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 63 | "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 64 | "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 65 | "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 66 | ] 67 | 68 | [[package]] 69 | name = "conduit-mime-types" 70 | version = "0.7.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | dependencies = [ 73 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 74 | ] 75 | 76 | [[package]] 77 | name = "cookie" 78 | version = "0.2.4" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | dependencies = [ 81 | "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", 82 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 83 | "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 84 | "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 85 | ] 86 | 87 | [[package]] 88 | name = "dotenv" 89 | version = "0.8.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | dependencies = [ 92 | "regex 0.1.71 (registry+https://github.com/rust-lang/crates.io-index)", 93 | ] 94 | 95 | [[package]] 96 | name = "encoding" 97 | version = "0.2.32" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | dependencies = [ 100 | "encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", 101 | "encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", 102 | "encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", 103 | "encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", 104 | "encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", 105 | ] 106 | 107 | [[package]] 108 | name = "encoding-index-japanese" 109 | version = "1.20141219.5" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | dependencies = [ 112 | "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 113 | ] 114 | 115 | [[package]] 116 | name = "encoding-index-korean" 117 | version = "1.20141219.5" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | dependencies = [ 120 | "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 121 | ] 122 | 123 | [[package]] 124 | name = "encoding-index-simpchinese" 125 | version = "1.20141219.5" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | dependencies = [ 128 | "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 129 | ] 130 | 131 | [[package]] 132 | name = "encoding-index-singlebyte" 133 | version = "1.20141219.5" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | dependencies = [ 136 | "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 137 | ] 138 | 139 | [[package]] 140 | name = "encoding-index-tradchinese" 141 | version = "1.20141219.5" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | dependencies = [ 144 | "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 145 | ] 146 | 147 | [[package]] 148 | name = "encoding_index_tests" 149 | version = "0.1.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | 152 | [[package]] 153 | name = "error" 154 | version = "0.1.9" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | dependencies = [ 157 | "traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 158 | "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 159 | ] 160 | 161 | [[package]] 162 | name = "flate2" 163 | version = "0.2.14" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | dependencies = [ 166 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 167 | "miniz-sys 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 168 | ] 169 | 170 | [[package]] 171 | name = "gcc" 172 | version = "0.3.28" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | 175 | [[package]] 176 | name = "gdi32-sys" 177 | version = "0.2.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | dependencies = [ 180 | "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 181 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 182 | ] 183 | 184 | [[package]] 185 | name = "hex" 186 | version = "0.2.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | 189 | [[package]] 190 | name = "hpack" 191 | version = "0.2.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | dependencies = [ 194 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 195 | ] 196 | 197 | [[package]] 198 | name = "httparse" 199 | version = "1.1.2" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | 202 | [[package]] 203 | name = "hyper" 204 | version = "0.8.1" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | dependencies = [ 207 | "cookie 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 208 | "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 209 | "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 210 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 211 | "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 212 | "num_cpus 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", 213 | "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", 214 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 215 | "serde 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 216 | "solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", 217 | "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 218 | "traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 219 | "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 220 | "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 221 | "url 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", 222 | ] 223 | 224 | [[package]] 225 | name = "hyper" 226 | version = "0.9.6" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | dependencies = [ 229 | "cookie 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 230 | "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 231 | "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 232 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 233 | "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 234 | "num_cpus 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", 235 | "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", 236 | "openssl-verify 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 237 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 238 | "serde 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 239 | "solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", 240 | "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 241 | "traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 242 | "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 243 | "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 244 | "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 245 | ] 246 | 247 | [[package]] 248 | name = "hyperdav" 249 | version = "0.1.2" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | dependencies = [ 252 | "hyper 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", 253 | "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 254 | "xml-rs 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 255 | ] 256 | 257 | [[package]] 258 | name = "id3" 259 | version = "0.1.10" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | dependencies = [ 262 | "byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", 263 | "encoding 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)", 264 | "flate2 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", 265 | "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 266 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 267 | "num 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 268 | "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 269 | ] 270 | 271 | [[package]] 272 | name = "idna" 273 | version = "0.1.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | dependencies = [ 276 | "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 277 | "unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 278 | "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 279 | ] 280 | 281 | [[package]] 282 | name = "iron" 283 | version = "0.3.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | dependencies = [ 286 | "conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", 287 | "error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 288 | "hyper 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 289 | "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", 290 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 291 | "modifier 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 292 | "num_cpus 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", 293 | "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 294 | "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 295 | "url 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", 296 | ] 297 | 298 | [[package]] 299 | name = "jamendo" 300 | version = "0.1.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | dependencies = [ 303 | "hyper 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", 304 | "serde 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 305 | "serde_json 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", 306 | "serde_macros 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 307 | "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 308 | ] 309 | 310 | [[package]] 311 | name = "kernel32-sys" 312 | version = "0.2.2" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | dependencies = [ 315 | "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 316 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 317 | ] 318 | 319 | [[package]] 320 | name = "language-tags" 321 | version = "0.2.2" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | 324 | [[package]] 325 | name = "lazy_static" 326 | version = "0.1.16" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | 329 | [[package]] 330 | name = "lazy_static" 331 | version = "0.2.1" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | 334 | [[package]] 335 | name = "libc" 336 | version = "0.2.11" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | 339 | [[package]] 340 | name = "libressl-pnacl-sys" 341 | version = "2.1.6" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | dependencies = [ 344 | "pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)", 345 | ] 346 | 347 | [[package]] 348 | name = "log" 349 | version = "0.3.6" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | 352 | [[package]] 353 | name = "logger" 354 | version = "0.0.3" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | dependencies = [ 357 | "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 358 | "term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", 359 | "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 360 | ] 361 | 362 | [[package]] 363 | name = "matches" 364 | version = "0.1.2" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | 367 | [[package]] 368 | name = "memchr" 369 | version = "0.1.11" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | dependencies = [ 372 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 373 | ] 374 | 375 | [[package]] 376 | name = "mime" 377 | version = "0.2.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | dependencies = [ 380 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 381 | "serde 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 382 | ] 383 | 384 | [[package]] 385 | name = "miniz-sys" 386 | version = "0.1.7" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | dependencies = [ 389 | "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", 390 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 391 | ] 392 | 393 | [[package]] 394 | name = "modifier" 395 | version = "0.1.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | 398 | [[package]] 399 | name = "num" 400 | version = "0.1.32" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | dependencies = [ 403 | "num-bigint 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 404 | "num-complex 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 405 | "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 406 | "num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 407 | "num-rational 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 408 | "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 409 | ] 410 | 411 | [[package]] 412 | name = "num-bigint" 413 | version = "0.1.32" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | dependencies = [ 416 | "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 417 | "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 418 | "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 419 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 420 | ] 421 | 422 | [[package]] 423 | name = "num-complex" 424 | version = "0.1.32" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | dependencies = [ 427 | "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 428 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 429 | ] 430 | 431 | [[package]] 432 | name = "num-integer" 433 | version = "0.1.32" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | dependencies = [ 436 | "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 437 | ] 438 | 439 | [[package]] 440 | name = "num-iter" 441 | version = "0.1.32" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | dependencies = [ 444 | "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 445 | "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 446 | ] 447 | 448 | [[package]] 449 | name = "num-rational" 450 | version = "0.1.32" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | dependencies = [ 453 | "num-bigint 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 454 | "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 455 | "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 456 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 457 | ] 458 | 459 | [[package]] 460 | name = "num-traits" 461 | version = "0.1.32" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | 464 | [[package]] 465 | name = "num_cpus" 466 | version = "0.2.12" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | dependencies = [ 469 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 470 | ] 471 | 472 | [[package]] 473 | name = "openssl" 474 | version = "0.7.13" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | dependencies = [ 477 | "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 478 | "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", 479 | "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 480 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 481 | "openssl-sys 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", 482 | "openssl-sys-extras 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", 483 | ] 484 | 485 | [[package]] 486 | name = "openssl-sys" 487 | version = "0.7.13" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | dependencies = [ 490 | "gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 491 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 492 | "libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 493 | "pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 494 | "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 495 | ] 496 | 497 | [[package]] 498 | name = "openssl-sys-extras" 499 | version = "0.7.13" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | dependencies = [ 502 | "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", 503 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 504 | "openssl-sys 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", 505 | ] 506 | 507 | [[package]] 508 | name = "openssl-verify" 509 | version = "0.1.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | dependencies = [ 512 | "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", 513 | ] 514 | 515 | [[package]] 516 | name = "pkg-config" 517 | version = "0.3.8" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | 520 | [[package]] 521 | name = "plugin" 522 | version = "0.2.6" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | dependencies = [ 525 | "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 526 | ] 527 | 528 | [[package]] 529 | name = "pnacl-build-helper" 530 | version = "1.4.10" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | dependencies = [ 533 | "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 534 | ] 535 | 536 | [[package]] 537 | name = "quasi" 538 | version = "0.11.0" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | 541 | [[package]] 542 | name = "quasi_codegen" 543 | version = "0.11.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | dependencies = [ 546 | "aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", 547 | ] 548 | 549 | [[package]] 550 | name = "quasi_macros" 551 | version = "0.11.0" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | dependencies = [ 554 | "quasi_codegen 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 555 | ] 556 | 557 | [[package]] 558 | name = "rand" 559 | version = "0.3.14" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | dependencies = [ 562 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 563 | ] 564 | 565 | [[package]] 566 | name = "regex" 567 | version = "0.1.71" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | dependencies = [ 570 | "aho-corasick 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", 571 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 572 | "regex-syntax 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 573 | "thread_local 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 574 | "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 575 | ] 576 | 577 | [[package]] 578 | name = "regex-syntax" 579 | version = "0.3.3" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | 582 | [[package]] 583 | name = "route-recognizer" 584 | version = "0.1.11" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | 587 | [[package]] 588 | name = "router" 589 | version = "0.1.1" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | dependencies = [ 592 | "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 593 | "route-recognizer 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 594 | ] 595 | 596 | [[package]] 597 | name = "rustc-serialize" 598 | version = "0.3.19" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | 601 | [[package]] 602 | name = "rustc_version" 603 | version = "0.1.7" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | dependencies = [ 606 | "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", 607 | ] 608 | 609 | [[package]] 610 | name = "semver" 611 | version = "0.1.20" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | 614 | [[package]] 615 | name = "serde" 616 | version = "0.7.7" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | 619 | [[package]] 620 | name = "serde_codegen" 621 | version = "0.7.7" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | dependencies = [ 624 | "aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", 625 | "quasi 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 626 | "quasi_macros 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 627 | ] 628 | 629 | [[package]] 630 | name = "serde_json" 631 | version = "0.7.1" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | dependencies = [ 634 | "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 635 | "serde 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 636 | ] 637 | 638 | [[package]] 639 | name = "serde_macros" 640 | version = "0.7.7" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | dependencies = [ 643 | "serde_codegen 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 644 | ] 645 | 646 | [[package]] 647 | name = "solicit" 648 | version = "0.4.4" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | dependencies = [ 651 | "hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 652 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 653 | ] 654 | 655 | [[package]] 656 | name = "tempdir" 657 | version = "0.3.4" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | dependencies = [ 660 | "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 661 | ] 662 | 663 | [[package]] 664 | name = "term" 665 | version = "0.4.4" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | dependencies = [ 668 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 669 | "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 670 | ] 671 | 672 | [[package]] 673 | name = "thread-id" 674 | version = "2.0.0" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | dependencies = [ 677 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 678 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 679 | ] 680 | 681 | [[package]] 682 | name = "thread_local" 683 | version = "0.2.6" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | dependencies = [ 686 | "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 687 | ] 688 | 689 | [[package]] 690 | name = "time" 691 | version = "0.1.35" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | dependencies = [ 694 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 695 | "libc 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 696 | "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 697 | ] 698 | 699 | [[package]] 700 | name = "traitobject" 701 | version = "0.0.1" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | 704 | [[package]] 705 | name = "traitobject" 706 | version = "0.0.3" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | 709 | [[package]] 710 | name = "typeable" 711 | version = "0.1.2" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | 714 | [[package]] 715 | name = "typemap" 716 | version = "0.3.3" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | dependencies = [ 719 | "unsafe-any 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", 720 | ] 721 | 722 | [[package]] 723 | name = "unicase" 724 | version = "1.4.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | dependencies = [ 727 | "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 728 | ] 729 | 730 | [[package]] 731 | name = "unicode-bidi" 732 | version = "0.2.3" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | dependencies = [ 735 | "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 736 | ] 737 | 738 | [[package]] 739 | name = "unicode-normalization" 740 | version = "0.1.2" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | 743 | [[package]] 744 | name = "unsafe-any" 745 | version = "0.4.1" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | dependencies = [ 748 | "traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 749 | ] 750 | 751 | [[package]] 752 | name = "url" 753 | version = "0.5.9" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | dependencies = [ 756 | "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 757 | "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", 758 | "unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 759 | "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 760 | "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 761 | ] 762 | 763 | [[package]] 764 | name = "url" 765 | version = "1.1.1" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | dependencies = [ 768 | "idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 769 | "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 770 | "serde 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 771 | ] 772 | 773 | [[package]] 774 | name = "user32-sys" 775 | version = "0.2.0" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | dependencies = [ 778 | "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 779 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 780 | ] 781 | 782 | [[package]] 783 | name = "utf8-ranges" 784 | version = "0.1.3" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | 787 | [[package]] 788 | name = "uuid" 789 | version = "0.2.2" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | dependencies = [ 792 | "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 793 | "serde 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", 794 | ] 795 | 796 | [[package]] 797 | name = "walkdir" 798 | version = "0.1.5" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | dependencies = [ 801 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 802 | "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 803 | ] 804 | 805 | [[package]] 806 | name = "winapi" 807 | version = "0.2.7" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | 810 | [[package]] 811 | name = "winapi-build" 812 | version = "0.1.1" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | 815 | [[package]] 816 | name = "xml-rs" 817 | version = "0.3.4" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | dependencies = [ 820 | "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 821 | ] 822 | 823 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cloudfm" 3 | version = "0.1.0" 4 | authors = ["Jakob Gillich "] 5 | license = "MIT" 6 | 7 | [dependencies] 8 | hyper = "0.9.6" 9 | url = { version = "1.1.1", features = [ "serde" ] } 10 | iron = "0.3.0" 11 | router = "0.1.1" 12 | logger = "0.0.3" 13 | id3 = "0.1.10" 14 | walkdir = "0.1.5" 15 | serde = "0.7.5" 16 | serde_json = "0.7.1" 17 | serde_macros = "0.7.5" 18 | dotenv = "0.8.0" 19 | chill = { version = "*", git = "https://github.com/jgillich/chill.git" } 20 | uuid = "0.2.2" 21 | jamendo = "0.1.0" 22 | hex = "0.2.0" 23 | lazy_static = "0.2.1" 24 | hyperdav = "0.1.2" 25 | -------------------------------------------------------------------------------- /lib/indexd.dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:8 2 | RUN apt update && apt upgrade -y && apt install curl file sudo gcc libssl-dev -y && \ 3 | curl -sSf https://static.rust-lang.org/rustup.sh | sh -s -- --channel=nightly && \ 4 | groupadd -r cloudfm && useradd -r -g cloudfm cloudfm 5 | COPY . /src 6 | ENV PATH /usr/local/bin:$PATH 7 | RUN cd /src && cargo build --bin indexd --release && cp /src/target/release/indexd /usr/local/bin && \ 8 | rm -rf /src 9 | USER cloudfm 10 | CMD indexd 11 | -------------------------------------------------------------------------------- /lib/proxyd.dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:8 2 | RUN apt update && apt upgrade -y && apt install curl file sudo gcc libssl-dev -y && \ 3 | curl -sSf https://static.rust-lang.org/rustup.sh | sh -s -- --channel=nightly && \ 4 | groupadd -r cloudfm && useradd -r -g cloudfm cloudfm 5 | COPY . /src 6 | ENV PATH /usr/local/bin:$PATH 7 | RUN cd /src && cargo build --bin proxyd --release && cp /src/target/release/proxyd /usr/local/bin && \ 8 | rm -rf /src 9 | USER cloudfm 10 | CMD proxyd 11 | -------------------------------------------------------------------------------- /lib/src/album.rs: -------------------------------------------------------------------------------- 1 | use chill::DocumentId; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | pub struct Album { 5 | #[serde(rename="type")] 6 | _type: String, 7 | pub name: String, 8 | pub artist: DocumentId, 9 | } 10 | 11 | impl Album { 12 | pub fn new(name: &str, artist: DocumentId) -> Self { 13 | Album { 14 | _type: "album".into(), 15 | name: name.into(), 16 | artist: artist, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/artist.rs: -------------------------------------------------------------------------------- 1 | #[derive(Serialize, Deserialize, Debug)] 2 | pub struct Artist { 3 | #[serde(rename="type")] 4 | _type: String, 5 | pub name: String, 6 | } 7 | 8 | impl Artist { 9 | pub fn new(name: &str) -> Self { 10 | Artist { 11 | _type: "artist".into(), 12 | name: name.into(), 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/bin/debug.rs: -------------------------------------------------------------------------------- 1 | extern crate cloudfm; 2 | 3 | pub fn main() { 4 | println!("machine_id: {}", cloudfm::MACHINE_ID.to_string()); 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/bin/indexd.rs: -------------------------------------------------------------------------------- 1 | #![feature(question_mark)] 2 | 3 | extern crate cloudfm; 4 | extern crate chill; 5 | extern crate dotenv; 6 | extern crate serde_json; 7 | 8 | use cloudfm::{Index, Indexer, views, User, Error, Backend}; 9 | use chill::{ViewRow, DocumentId, AllDocumentsViewValue}; 10 | use std::env; 11 | use dotenv::dotenv; 12 | 13 | pub fn main() { 14 | dotenv().ok(); 15 | 16 | let db_url = env::var("DATABASE_URL").unwrap_or("http://localhost:5984".into()); 17 | let db = chill::Client::new(&db_url).expect("DATABASE_URL must be a valid URL"); 18 | 19 | let (count, errors) = index_all(db); 20 | println!("index_all done. success: {}, failed: {}", 21 | count - errors.len(), 22 | errors.len()); 23 | for error in errors { 24 | println!("error: {:#?}", error); 25 | } 26 | } 27 | 28 | pub fn index_all(db: chill::Client) -> (usize, Vec) { 29 | let mut errors: Vec = Vec::new(); 30 | 31 | // skips design docs 32 | let start_key = &DocumentId::from("org.couchdb.user:"); 33 | 34 | let action = match db.read_all_documents("/_users") { 35 | Ok(action) => action.with_start_key(start_key), 36 | Err(error) => return (1, vec![Error::from(error)]), 37 | }; 38 | 39 | let res = match action.run() { 40 | Ok(res) => res, 41 | Err(error) => return (1, vec![Error::from(error)]), 42 | }; 43 | 44 | let view = match res.as_unreduced() { 45 | Some(view) => view, 46 | None => unimplemented!(), 47 | }; 48 | 49 | for row in view.rows() { 50 | if let Err(e) = index_user(&db, row) { 51 | errors.push(e); 52 | } 53 | } 54 | 55 | (view.rows().len(), errors) 56 | } 57 | 58 | 59 | pub fn index_user(db: &chill::Client, 60 | row: &ViewRow) 61 | -> Result<(), Error> { 62 | let doc = db.read_document(("/_users", row.key()))?.run()?; 63 | let user: User = doc.get_content()?; 64 | 65 | views::apply(db, &user.db_name())?; 66 | 67 | if let Some(ref backends) = user.backends { 68 | for backend in backends { 69 | match backend { 70 | &Backend::File(ref backend) => Index::index(db, &user, backend)?, 71 | &Backend::Jamendo(ref backend) => Index::index(db, &user, backend)?, 72 | &Backend::Webdav(ref backend) => Index::index(db, &user, backend)?, 73 | } 74 | } 75 | } 76 | 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/bin/manager.rs: -------------------------------------------------------------------------------- 1 | pub fn main() {} 2 | -------------------------------------------------------------------------------- /lib/src/bin/proxyd.rs: -------------------------------------------------------------------------------- 1 | extern crate cloudfm; 2 | extern crate chill; 3 | extern crate iron; 4 | extern crate router; 5 | extern crate logger; 6 | extern crate dotenv; 7 | extern crate serde; 8 | 9 | use std::env; 10 | use iron::Iron; 11 | use iron::{IronResult, Request, Response}; 12 | use iron::middleware::Chain; 13 | use router::Router; 14 | use logger::Logger; 15 | use dotenv::dotenv; 16 | use cloudfm::{Uri, Proxy, ProxyHandler}; 17 | 18 | pub fn main() { 19 | dotenv().ok(); 20 | 21 | let (logger_before, logger_after) = Logger::new(None); 22 | 23 | let mut router = Router::new(); 24 | router.get("/tracks/:uri", handle); 25 | 26 | let mut chain = Chain::new(router); 27 | chain.link_before(logger_before); 28 | chain.link_after(logger_after); 29 | 30 | let addr = env::var("PROXYD_ADDR").unwrap_or("127.0.0.1:8423".into()); 31 | println!("proxyd starting on {}", addr); 32 | 33 | Iron::new(chain).http(addr.parse::().unwrap()).unwrap(); 34 | } 35 | 36 | fn handle(req: &mut Request) -> IronResult { 37 | let ref uri = req.extensions.get::().unwrap().find("uri").unwrap(); 38 | 39 | // FIXME proper format handling 40 | let uri = uri.replace(".mp3", ""); 41 | let uri = uri.parse::().unwrap(); 42 | 43 | match uri { 44 | Uri::File(u) => Proxy::handle(u), 45 | Uri::Jamendo(u) => Proxy::handle(u), 46 | Uri::Webdav(u) => Proxy::handle(u), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error as StdError; 3 | use chill; 4 | use jamendo; 5 | use uri; 6 | use views; 7 | use index; 8 | use hyperdav; 9 | 10 | trait_enum! { 11 | enum Error: StdError { 12 | Env(env::VarError), 13 | Chill(chill::Error), 14 | Jamendo(jamendo::Error), 15 | View(views::ViewError), 16 | UriParse(uri::UriParseError), 17 | Index(index::IndexError), 18 | Webdav(hyperdav::Error), 19 | } 20 | } 21 | 22 | impl From for Error { 23 | fn from(err: env::VarError) -> Error { 24 | Error::Env(err) 25 | } 26 | } 27 | 28 | impl From for Error { 29 | fn from(err: chill::Error) -> Error { 30 | Error::Chill(err) 31 | } 32 | } 33 | 34 | impl From for Error { 35 | fn from(err: jamendo::Error) -> Error { 36 | Error::Jamendo(err) 37 | } 38 | } 39 | 40 | impl From for Error { 41 | fn from(err: views::ViewError) -> Error { 42 | Error::View(err) 43 | } 44 | } 45 | 46 | impl From for Error { 47 | fn from(err: uri::UriParseError) -> Error { 48 | Error::UriParse(err) 49 | } 50 | } 51 | 52 | impl From for Error { 53 | fn from(err: index::IndexError) -> Error { 54 | Error::Index(err) 55 | } 56 | } 57 | 58 | impl From for Error { 59 | fn from(err: hyperdav::Error) -> Error { 60 | Error::Webdav(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/index/file.rs: -------------------------------------------------------------------------------- 1 | use id3::Tag; 2 | use chill; 3 | use super::{Index, Indexer}; 4 | use walkdir::{DirEntry, WalkDir}; 5 | use {DecodedTrack, User, Error, FileBackend, Uri, FileUri, MACHINE_ID}; 6 | 7 | impl Indexer for Index { 8 | fn index(db: &chill::Client, user: &User, backend: &FileBackend) -> Result<(), Error> { 9 | 10 | let mut tracks: Vec = Vec::new(); 11 | 12 | for path in &backend.paths { 13 | for entry in walk_path(path) { 14 | if let Ok(tag) = Tag::read_from_path(entry.path()) { 15 | if let Some(file_path) = entry.path().to_str() { 16 | let uri = Uri::File(FileUri::new(&MACHINE_ID.to_string(), file_path)); 17 | 18 | if let Some(track) = DecodedTrack::from_tag(tag, uri) { 19 | tracks.push(track); 20 | } 21 | } 22 | } 23 | } 24 | } 25 | 26 | Index::take_result(db, &user.db_name(), tracks)?; 27 | Ok(()) 28 | } 29 | } 30 | 31 | fn is_file_type(e: &DirEntry, ext: &str) -> bool { 32 | let p = e.path(); 33 | p.is_file() && p.extension().map_or(false, |s| s == ext) 34 | } 35 | 36 | 37 | fn is_music(e: &DirEntry) -> bool { 38 | is_file_type(e, "mp3") || is_file_type(e, "ogg") 39 | } 40 | 41 | fn walk_path(path: &str) -> Vec { 42 | WalkDir::new(path) 43 | .into_iter() 44 | .filter_map(|e| e.ok()) 45 | .filter(|e| is_music(e)) 46 | .collect() 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/index/jamendo.rs: -------------------------------------------------------------------------------- 1 | 2 | use chill; 3 | use jamendo; 4 | use super::{Index, IndexError, Indexer}; 5 | use {DecodedTrack, JamendoUri, Uri, User, Error, JamendoBackend}; 6 | 7 | impl Indexer for Index { 8 | fn index(db: &chill::Client, user: &User, backend: &JamendoBackend) -> Result<(), Error> { 9 | let mut decoded: Vec = Vec::new(); 10 | 11 | let jamendo = jamendo::Client::new(jamendo::TEST_ID); // FIXME we shall not use TEST_ID 12 | 13 | let jamendo_user = jamendo.get_users().user_name(&backend.user_name).run()?; 14 | let jamendo_user = jamendo_user.first().ok_or(IndexError::UserNotFound)?; 15 | 16 | for user in jamendo.get_users_tracks().user_id(jamendo_user.id).run()? { 17 | for track in user.tracks { 18 | // users/tracks does not include position, so we have to fetch the track again 19 | if let Some(track) = jamendo.get_tracks().track_id(track.id).run()?.first() { 20 | decoded.push(DecodedTrack { 21 | artist: track.artist_name.clone(), 22 | album: track.album_name.clone(), 23 | name: track.name.clone(), 24 | number: track.position, 25 | uri: Uri::Jamendo(JamendoUri::new(&backend.id, track.id)), 26 | }); 27 | } 28 | } 29 | } 30 | 31 | Index::take_result(db, &user.db_name(), decoded)?; 32 | Ok(()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/index/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt}; 2 | use chill; 3 | use chill::IntoDatabasePath; 4 | use {views, DecodedTrack, User, Error, Artist, Album, Track}; 5 | 6 | mod file; 7 | mod jamendo; 8 | mod webdav; 9 | 10 | pub struct Index; 11 | 12 | impl Index { 13 | pub fn take_result<'a, P>(db: &'a chill::Client, 14 | db_path: P, 15 | result: Vec) 16 | -> Result<(), Error> 17 | where P: IntoDatabasePath<'a> 18 | { 19 | let db_path = db_path.into_database_path()?; 20 | 21 | let mut tracks = views::all_tracks(db, db_path)?; 22 | let mut albums = views::all_albums(db, db_path)?; 23 | let mut artists = views::all_artists(db, db_path)?; 24 | 25 | for track in result { 26 | 27 | // Find or create artists 28 | let artist_id = match artists.iter().find(|&&(_, ref a)| a == &track.artist) { 29 | Some(&(ref id, _)) => Some(id.clone()), 30 | None => None, 31 | }; 32 | let artist_id = match artist_id { 33 | Some(id) => id, 34 | None => { 35 | let artist = Artist::new(&track.artist); 36 | let (id, _) = db.create_document(db_path, &artist)?.run()?; 37 | artists.push((id.clone(), track.artist.clone())); 38 | id 39 | } 40 | }; 41 | 42 | // Find or create album 43 | let album_id = { 44 | let mut id = None; 45 | let matches = albums.iter().filter(|&&(_, ref a)| a == &track.album); 46 | for &(ref doc_id, _) in matches { 47 | let doc = db.read_document((db_path, doc_id))?.run()?; 48 | let album: Album = doc.get_content()?; 49 | if album.artist == artist_id { 50 | id = Some(doc_id.clone()); 51 | break; // this should never be needed, maybe add a panic? 52 | }; 53 | } 54 | id 55 | }; 56 | let album_id = match album_id { 57 | Some(id) => id.clone(), 58 | None => { 59 | let album = Album::new(&track.album, artist_id.clone()); 60 | let (id, _) = db.create_document(db_path, &album)?.run()?; 61 | albums.push((id.clone(), track.album.clone())); 62 | id 63 | } 64 | }; 65 | 66 | // Find or create track 67 | let track_id = { 68 | let mut id = None; 69 | let matches = tracks.iter().filter(|&&(_, ref a)| a == &track.name); 70 | for &(ref doc_id, _) in matches { 71 | let doc = db.read_document((db_path, doc_id))?.run()?; 72 | let track: Track = doc.get_content()?; 73 | if track.artist == artist_id && track.album == album_id { 74 | id = Some(doc_id.clone()); 75 | break; // this should never be needed, maybe add a panic? 76 | }; 77 | } 78 | id 79 | }; 80 | match track_id { 81 | Some(_) => (), 82 | None => { 83 | let track = Track::new(&track.name, 84 | track.number, 85 | artist_id, 86 | album_id, 87 | vec![track.uri]); 88 | let (id, _) = db.create_document(db_path, &track)?.run()?; 89 | tracks.push((id, track.name)); 90 | } 91 | }; 92 | } 93 | 94 | Ok(()) 95 | } 96 | } 97 | 98 | pub trait Indexer { 99 | fn index(&chill::Client, &User, &T) -> Result<(), Error>; 100 | } 101 | 102 | #[derive(Debug)] 103 | pub enum IndexError { 104 | UserNotFound, 105 | } 106 | 107 | impl error::Error for IndexError { 108 | fn description(&self) -> &str { 109 | match *self { 110 | IndexError::UserNotFound => "user was not found", 111 | } 112 | } 113 | 114 | fn cause(&self) -> Option<&error::Error> { 115 | None 116 | } 117 | } 118 | 119 | impl fmt::Display for IndexError { 120 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 121 | write!(f, "{}", error::Error::description(self)) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/src/index/webdav.rs: -------------------------------------------------------------------------------- 1 | use id3::Tag; 2 | use chill; 3 | use hyperdav::webdav; 4 | use super::{Index, Indexer}; 5 | use {DecodedTrack, WebdavUri, Uri, User, Error, WebdavBackend}; 6 | 7 | impl Indexer for Index { 8 | fn index(db: &chill::Client, user: &User, backend: &WebdavBackend) -> Result<(), Error> { 9 | let client = webdav::Client::new(); 10 | let mut tracks: Vec = Vec::new(); 11 | 12 | for e in client.ls(backend.webdav_url.clone())? { 13 | if e.href.ends_with(".mp3") || e.href.ends_with("ogg") { 14 | if let Ok(mut res) = client.get(&e.href) { 15 | if let Ok(tag) = Tag::read_from(&mut res) { 16 | let uri = Uri::Webdav(WebdavUri::new(&backend.id, &e.href)); 17 | 18 | if let Some(track) = DecodedTrack::from_tag(tag, uri) { 19 | tracks.push(track); 20 | } 21 | } 22 | } 23 | } 24 | } 25 | 26 | 27 | Index::take_result(db, &user.db_name(), tracks)?; 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(custom_derive, plugin, question_mark, slice_patterns)] 2 | #![plugin(serde_macros)] 3 | 4 | #![feature(type_ascription)] 5 | 6 | extern crate hyper; 7 | extern crate url; 8 | extern crate iron; 9 | extern crate logger; 10 | extern crate router; 11 | extern crate walkdir; 12 | extern crate id3; 13 | extern crate chill; 14 | extern crate dotenv; 15 | extern crate uuid; 16 | extern crate serde; 17 | extern crate serde_json; 18 | extern crate jamendo; 19 | extern crate hyperdav; 20 | extern crate hex; 21 | #[macro_use] 22 | extern crate lazy_static; 23 | 24 | // TODO support visibility modifier instead of using pub 25 | macro_rules! trait_enum { 26 | 27 | (enum $name:ident: $_trait:ident { $($var:ident($ty:ty)),*, }) => { 28 | #[derive(Debug)] 29 | pub enum $name { 30 | $( 31 | $var($ty), 32 | )* 33 | } 34 | 35 | use std::ops::Deref; 36 | impl<'a> Deref for $name { 37 | type Target = ($_trait + 'a); 38 | fn deref(&self) -> &$_trait { 39 | match *self { 40 | $($name::$var(ref x) => x,)* 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | lazy_static! { 48 | pub static ref MACHINE_ID: uuid::Uuid = { 49 | let file = std::fs::File::open("/etc/machine-id").unwrap(); 50 | let buf = std::io::BufReader::new(file); 51 | let mut line = std::io::BufRead::lines(buf).next().unwrap().unwrap(); 52 | line.truncate(32); 53 | uuid::Uuid::parse_str(&line).unwrap() 54 | }; 55 | } 56 | 57 | pub mod index; 58 | pub mod proxy; 59 | pub mod album; 60 | pub mod artist; 61 | pub mod error; 62 | pub mod track; 63 | pub mod uri; 64 | pub mod user; 65 | pub mod views; 66 | 67 | pub use index::{Index, Indexer}; 68 | pub use proxy::{Proxy, ProxyHandler}; 69 | pub use album::Album; 70 | pub use artist::Artist; 71 | pub use error::*; 72 | pub use track::{Track, DecodedTrack}; 73 | pub use uri::{Uri, FileUri, JamendoUri, WebdavUri}; 74 | pub use user::{User, Backend, FileBackend, JamendoBackend, WebdavBackend}; 75 | -------------------------------------------------------------------------------- /lib/src/proxy/file.rs: -------------------------------------------------------------------------------- 1 | use iron::{IronResult, Response, status}; 2 | use iron::headers::{ContentType, ContentLength}; 3 | use hyper::mime::{Mime, TopLevel, SubLevel}; 4 | use std::fs::File; 5 | use FileUri; 6 | use super::{ProxyHandler, Proxy}; 7 | 8 | impl ProxyHandler for Proxy { 9 | fn handle(uri: FileUri) -> IronResult { 10 | match File::open(uri.file_path) { 11 | Ok(file) => { 12 | let length = file.metadata().unwrap().len(); 13 | let mut res = Response::with((status::Ok, file)); 14 | res.headers.set(ContentLength(length)); 15 | res.headers 16 | .set(ContentType(Mime(TopLevel::Audio, SubLevel::Ext("mpeg".into()), vec![]))); 17 | Ok(res) 18 | } 19 | // TODO better error handling 20 | Err(_) => Ok(Response::with((status::NotFound, "file not found"))), 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/proxy/jamendo.rs: -------------------------------------------------------------------------------- 1 | use iron::{IronResult, Response, status}; 2 | use JamendoUri; 3 | use super::{ProxyHandler, Proxy}; 4 | use hyper; 5 | use iron::headers::ContentType; 6 | use hyper::mime::{Mime, TopLevel, SubLevel}; 7 | use iron::response::BodyReader; 8 | use jamendo; 9 | use std::error::Error; 10 | 11 | impl ProxyHandler for Proxy { 12 | fn handle(uri: JamendoUri) -> IronResult { 13 | // TODO make id configurable 14 | let jamendo_client = jamendo::Client::new(jamendo::TEST_ID); 15 | 16 | let tracks = match jamendo_client.get_tracks().track_id(uri.jamendo_id).run() { 17 | Ok(tracks) => tracks, 18 | Err(err) => return Ok(Response::with((status::InternalServerError, err.description()))), 19 | }; 20 | 21 | let track = match tracks.first() { 22 | Some(track) => track, 23 | None => return Ok(Response::with((status::NotFound, "file not found"))), 24 | }; 25 | 26 | let track = match hyper::Client::new().get(&track.audiodownload).send() { 27 | Ok(track) => track, 28 | Err(err) => return Ok(Response::with((status::InternalServerError, err.description()))), 29 | }; 30 | 31 | let mut res = Response::with((status::Ok, BodyReader(track))); 32 | res.headers.set(ContentType(Mime(TopLevel::Audio, SubLevel::Ext("mpeg".into()), vec![]))); 33 | Ok(res) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/proxy/mod.rs: -------------------------------------------------------------------------------- 1 | use iron::{IronResult, Response}; 2 | 3 | mod file; 4 | mod jamendo; 5 | mod webdav; 6 | 7 | pub struct Proxy; 8 | 9 | pub trait ProxyHandler { 10 | fn handle(T) -> IronResult; 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/proxy/webdav.rs: -------------------------------------------------------------------------------- 1 | use iron::{IronResult, Response}; 2 | use WebdavUri; 3 | use super::{ProxyHandler, Proxy}; 4 | 5 | impl ProxyHandler for Proxy { 6 | fn handle(uri: WebdavUri) -> IronResult { 7 | unimplemented!(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/track.rs: -------------------------------------------------------------------------------- 1 | use chill::DocumentId; 2 | use id3::Tag; 3 | use Uri; 4 | 5 | #[derive(Serialize, Deserialize, Debug)] 6 | pub struct Track { 7 | #[serde(rename="type")] 8 | _type: String, 9 | pub name: String, 10 | pub number: u32, 11 | pub artist: DocumentId, 12 | pub album: DocumentId, 13 | pub uris: Vec, 14 | } 15 | 16 | impl Track { 17 | pub fn new(name: &str, 18 | number: u32, 19 | artist: DocumentId, 20 | album: DocumentId, 21 | uris: Vec) 22 | -> Self { 23 | Track { 24 | _type: "track".into(), 25 | name: name.into(), 26 | number: number, 27 | artist: artist, 28 | album: album, 29 | uris: uris, 30 | } 31 | } 32 | } 33 | 34 | #[derive(Serialize, Deserialize, Debug)] 35 | pub struct DecodedTrack { 36 | pub name: String, 37 | pub number: u32, 38 | pub artist: String, 39 | pub album: String, 40 | pub uri: Uri, 41 | } 42 | 43 | impl DecodedTrack { 44 | pub fn from_tag(tag: Tag, uri: Uri) -> Option { 45 | 46 | let artist = match tag.artist() { 47 | Some(artist) => artist, 48 | None => return None, 49 | }; 50 | 51 | let album = match tag.album() { 52 | Some(album) => album, 53 | None => return None, 54 | }; 55 | 56 | let name = match tag.title() { 57 | Some(name) => name, 58 | None => return None, 59 | }; 60 | 61 | let number = match tag.track() { 62 | Some(number) => number, 63 | None => return None, 64 | }; 65 | 66 | Some(DecodedTrack { 67 | artist: artist.into(), 68 | album: album.into(), 69 | name: name.into(), 70 | number: number, 71 | uri: uri, 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/uri.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, error}; 2 | use std::str::FromStr; 3 | use serde; 4 | use hex::{ToHex, FromHex}; 5 | use std::error::Error; 6 | 7 | #[derive(Debug, PartialEq)] 8 | pub enum Uri { 9 | File(FileUri), 10 | Jamendo(JamendoUri), 11 | Webdav(WebdavUri), 12 | } 13 | 14 | impl fmt::Display for Uri { 15 | fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> { 16 | match *self { 17 | Uri::File(ref uri) => { 18 | write!(formatter, 19 | "file:{}:{}", 20 | uri.backend_id, 21 | uri.file_path.to_hex()) 22 | } 23 | Uri::Jamendo(ref uri) => { 24 | write!(formatter, "jamendo:{}:{}", uri.backend_id, uri.jamendo_id) 25 | } 26 | Uri::Webdav(ref uri) => { 27 | write!(formatter, 28 | "webdav:{}:{}", 29 | uri.backend_id, 30 | uri.file_path.to_hex()) 31 | } 32 | } 33 | } 34 | } 35 | 36 | impl FromStr for Uri { 37 | type Err = UriParseError; 38 | 39 | fn from_str(s: &str) -> Result { 40 | let parts: Vec<&str> = s.split(':').collect(); 41 | match &parts[..] { 42 | ["file", backend_id, file_path] => { 43 | Ok(Uri::File(FileUri { 44 | backend_id: backend_id.into(), 45 | file_path: String::from_utf8(Vec::from_hex(file_path) 46 | .map_err(|_| UriParseError::UnknownFilePathEncoding)?) 47 | .map_err(|_| UriParseError::UnknownFilePathEncoding)?, 48 | })) 49 | } 50 | ["jamendo", backend_id, jamendo_id] => { 51 | Ok(Uri::Jamendo(JamendoUri { 52 | backend_id: backend_id.into(), 53 | jamendo_id: jamendo_id.parse().map_err(|_| UriParseError::InvalidFormat)?, 54 | })) 55 | } 56 | ["webdav", backend_id, file_path] => { 57 | Ok(Uri::Webdav(WebdavUri { 58 | backend_id: backend_id.into(), 59 | file_path: String::from_utf8(Vec::from_hex(file_path) 60 | .map_err(|_| UriParseError::UnknownFilePathEncoding)?) 61 | .map_err(|_| UriParseError::UnknownFilePathEncoding)?, 62 | })) 63 | } 64 | _ => Err(UriParseError::InvalidFormat), 65 | } 66 | } 67 | } 68 | 69 | #[derive(Debug)] 70 | pub enum UriParseError { 71 | UnknownFilePathEncoding, 72 | InvalidFormat, 73 | } 74 | 75 | impl error::Error for UriParseError { 76 | fn description(&self) -> &str { 77 | match *self { 78 | UriParseError::UnknownFilePathEncoding => { 79 | "file_path is not hex encoded or invalid UTF8" 80 | } 81 | UriParseError::InvalidFormat => "Invalid uri format", 82 | } 83 | } 84 | 85 | fn cause(&self) -> Option<&error::Error> { 86 | None 87 | } 88 | } 89 | 90 | impl fmt::Display for UriParseError { 91 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 92 | write!(f, "{}", error::Error::description(self)) 93 | } 94 | } 95 | 96 | 97 | impl serde::Serialize for Uri { 98 | fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> 99 | where S: serde::Serializer 100 | { 101 | serializer.serialize_str(&self.to_string()) 102 | } 103 | } 104 | 105 | impl serde::Deserialize for Uri { 106 | fn deserialize(deserializer: &mut D) -> Result 107 | where D: serde::Deserializer 108 | { 109 | struct Visitor; 110 | 111 | impl serde::de::Visitor for Visitor { 112 | type Value = Uri; 113 | 114 | fn visit_str(&mut self, value: &str) -> Result 115 | where E: serde::de::Error 116 | { 117 | value.parse::().map_err(|e| E::custom(e.description())) 118 | } 119 | } 120 | 121 | deserializer.deserialize(Visitor) 122 | } 123 | } 124 | 125 | #[derive(Debug, PartialEq)] 126 | pub struct FileUri { 127 | pub backend_id: String, 128 | pub file_path: String, 129 | } 130 | 131 | impl FileUri { 132 | pub fn new(backend_id: &str, file_path: &str) -> Self { 133 | FileUri { 134 | backend_id: backend_id.into(), 135 | file_path: file_path.into(), 136 | } 137 | } 138 | } 139 | 140 | #[derive(Debug, PartialEq)] 141 | pub struct JamendoUri { 142 | pub backend_id: String, 143 | pub jamendo_id: u32, 144 | } 145 | 146 | impl JamendoUri { 147 | pub fn new(backend_id: &str, jamendo_id: u32) -> Self { 148 | JamendoUri { 149 | backend_id: backend_id.into(), 150 | jamendo_id: jamendo_id, 151 | } 152 | } 153 | } 154 | 155 | #[derive(Debug, PartialEq)] 156 | pub struct WebdavUri { 157 | pub backend_id: String, 158 | pub file_path: String, 159 | } 160 | 161 | impl WebdavUri { 162 | pub fn new(backend_id: &str, file_path: &str) -> Self { 163 | WebdavUri { 164 | backend_id: backend_id.into(), 165 | file_path: file_path.into(), 166 | } 167 | } 168 | } 169 | 170 | #[cfg(test)] 171 | mod test { 172 | use super::*; 173 | 174 | #[test] 175 | fn file_uri() { 176 | let uri = Uri::File(FileUri { 177 | backend_id: "foo-bar".into(), 178 | file_path: "/home/baz".into(), 179 | }); 180 | let uri_str = uri.to_string(); 181 | assert_eq!(uri_str, "file:foo-bar:2f686f6d652f62617a"); 182 | assert_eq!(uri, uri_str.parse::().unwrap()); 183 | } 184 | 185 | #[test] 186 | fn jamendo_uri() { 187 | let uri = Uri::Jamendo(JamendoUri { 188 | backend_id: "foo-bar".into(), 189 | jamendo_id: 123, 190 | }); 191 | let uri_str = uri.to_string(); 192 | assert_eq!(uri_str, "jamendo:foo-bar:123"); 193 | assert_eq!(uri, uri_str.parse::().unwrap()); 194 | } 195 | 196 | #[test] 197 | fn webdav_uri() { 198 | let uri = Uri::Webdav(WebdavUri { 199 | backend_id: "foo-bar".into(), 200 | file_path: "/home/baz".into(), 201 | }); 202 | let uri_str = uri.to_string(); 203 | assert_eq!(uri_str, "webdav:foo-bar:2f686f6d652f62617a"); 204 | assert_eq!(uri, uri_str.parse::().unwrap()); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/src/user.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use url::Url; 3 | use serde; 4 | use serde_json; 5 | use chill::DatabaseName; 6 | use hex::ToHex; 7 | 8 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 9 | pub struct User { 10 | pub name: String, 11 | pub email: Option, 12 | pub backends: Option>, 13 | } 14 | 15 | impl User { 16 | pub fn db_name(&self) -> DatabaseName { 17 | let db_name = format!("userdb-{}", self.name.to_hex()); 18 | DatabaseName::from(db_name) 19 | } 20 | } 21 | 22 | #[derive(Debug, PartialEq)] 23 | pub enum Backend { 24 | File(FileBackend), 25 | Jamendo(JamendoBackend), 26 | Webdav(WebdavBackend), 27 | } 28 | 29 | impl serde::Serialize for Backend { 30 | fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> 31 | where S: serde::Serializer 32 | { 33 | match *self { 34 | Backend::File(ref backend) => backend.serialize::(serializer), 35 | Backend::Jamendo(ref backend) => backend.serialize::(serializer), 36 | Backend::Webdav(ref backend) => backend.serialize::(serializer), 37 | } 38 | } 39 | } 40 | 41 | impl serde::Deserialize for Backend { 42 | fn deserialize(de: &mut D) -> Result 43 | where D: serde::de::Deserializer 44 | { 45 | let mut object: BTreeMap = 46 | serde::de::Deserialize::deserialize(de)?; 47 | 48 | let object_type = match object.remove("type") { 49 | Some(v) => { 50 | match v.as_string() { 51 | Some(object_type) => object_type.to_string(), 52 | None => return Err(serde::de::Error::invalid_value("type is not a string")), 53 | } 54 | } 55 | None => return Err(serde::de::Error::missing_field("type")), 56 | }; 57 | 58 | let id = match object.remove("id") { 59 | Some(v) => { 60 | match v.as_string() { 61 | Some(id) => id.to_string(), 62 | None => return Err(serde::de::Error::invalid_value("id is not a string")), 63 | } 64 | } 65 | None => { 66 | return Err(serde::de::Error::missing_field("id")); 67 | } 68 | }; 69 | 70 | match object_type.as_ref() { 71 | "file" => { 72 | let machine_id = match object.remove("machine_id") { 73 | Some(v) => { 74 | match v.as_string() { 75 | Some(machine_id) => machine_id.to_string(), 76 | None => { 77 | return Err(serde::de::Error::invalid_value("machine_id is not a \ 78 | string")) 79 | } 80 | } 81 | } 82 | None => return Err(serde::de::Error::missing_field("machine_id")), 83 | }; 84 | 85 | let paths = match object.remove("paths") { 86 | Some(paths) => { 87 | serde_json::value::from_value(paths) 88 | .map_err(|_| { 89 | serde::de::Error::invalid_value("paths is not a string array") 90 | })? 91 | } 92 | None => return Err(serde::de::Error::missing_field("paths")), 93 | }; 94 | 95 | Ok(Backend::File(FileBackend { 96 | _type: "file".to_string(), 97 | id: id, 98 | machine_id: machine_id.into(), 99 | paths: paths, 100 | })) 101 | } 102 | "jamendo" => { 103 | let user_name = match object.remove("user_name") { 104 | Some(v) => { 105 | match v.as_string() { 106 | Some(user_name) => user_name.to_string(), 107 | None => { 108 | return Err(serde::de::Error::invalid_value("user_name is not a \ 109 | string")) 110 | } 111 | } 112 | } 113 | None => return Err(serde::de::Error::missing_field("user_name")), 114 | }; 115 | 116 | Ok(Backend::Jamendo(JamendoBackend { 117 | _type: "jamendo".to_string(), 118 | id: id, 119 | user_name: user_name, 120 | })) 121 | } 122 | "webdav" => { 123 | let webdav_url = match object.remove("webdav_url") { 124 | Some(v) => { 125 | match v.as_string() { 126 | Some(webdav_url) => { 127 | Url::parse(webdav_url) 128 | .map_err(|_| { 129 | serde::de::Error::invalid_value("webdav_url is not a url") 130 | })? 131 | } 132 | None => { 133 | return Err(serde::de::Error::invalid_value("webdav_url is not a \ 134 | string")) 135 | } 136 | } 137 | } 138 | None => return Err(serde::de::Error::missing_field("webdav_url")), 139 | }; 140 | 141 | Ok(Backend::Webdav(WebdavBackend { 142 | _type: "webdav".to_string(), 143 | id: id, 144 | webdav_url: webdav_url, 145 | })) 146 | } 147 | _ => Err(serde::de::Error::invalid_value("unkown type")), 148 | } 149 | } 150 | } 151 | 152 | 153 | // TODO implement custom serializer to get rid of _type 154 | #[derive(Serialize, Debug, PartialEq)] 155 | pub struct FileBackend { 156 | #[serde(rename="type")] 157 | pub _type: String, 158 | pub id: String, 159 | pub machine_id: String, 160 | pub paths: Vec, 161 | } 162 | 163 | // TODO implement custom serializer to get rid of _type 164 | #[derive(Serialize, Debug, PartialEq)] 165 | pub struct JamendoBackend { 166 | #[serde(rename="type")] 167 | pub _type: String, 168 | pub id: String, 169 | pub user_name: String, 170 | } 171 | 172 | // TODO implement custom serializer to get rid of _type 173 | #[derive(Serialize, Debug, PartialEq)] 174 | pub struct WebdavBackend { 175 | #[serde(rename="type")] 176 | pub _type: String, 177 | pub id: String, 178 | pub webdav_url: Url, 179 | } 180 | 181 | #[cfg(test)] 182 | mod test { 183 | use super::*; 184 | use serde_json; 185 | use url::Url; 186 | 187 | #[test] 188 | fn file_backend() { 189 | let uri = Backend::File(FileBackend { 190 | id: "123".into(), 191 | machine_id: "foo-bar".into(), 192 | paths: Vec::new(), 193 | _type: "file".into(), 194 | }); 195 | let uri_str = serde_json::to_string(&uri).unwrap(); 196 | assert_eq!(uri_str, 197 | "{\"type\":\"file\",\"id\":\"123\",\"machine_id\":\"foo-bar\",\"paths\":[]}"); 198 | assert_eq!(serde_json::from_str::(&uri_str).unwrap(), uri); 199 | } 200 | 201 | #[test] 202 | fn jamendo_backend() { 203 | let uri = Backend::Jamendo(JamendoBackend { 204 | id: "123".into(), 205 | user_name: "foo".into(), 206 | _type: "jamendo".into(), 207 | }); 208 | let uri_str = serde_json::to_string(&uri).unwrap(); 209 | assert_eq!(uri_str, 210 | "{\"type\":\"jamendo\",\"id\":\"123\",\"user_name\":\"foo\"}"); 211 | assert_eq!(serde_json::from_str::(&uri_str).unwrap(), uri); 212 | } 213 | 214 | #[test] 215 | fn webdav_backend() { 216 | let uri = Backend::Webdav(WebdavBackend { 217 | id: "123".into(), 218 | webdav_url: Url::parse("http://example.com").unwrap(), 219 | _type: "webdav".into(), 220 | }); 221 | let uri_str = serde_json::to_string(&uri).unwrap(); 222 | assert_eq!(uri_str, 223 | "{\"type\":\"webdav\",\"id\":\"123\",\"webdav_url\":\"http://example.com/\"}"); 224 | assert_eq!(serde_json::from_str::(&uri_str).unwrap(), uri); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lib/src/views.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt, default}; 2 | use serde_json; 3 | use chill; 4 | use chill::{DocumentId, IntoDatabasePath}; 5 | use Error; 6 | 7 | // IMPORTANT! 8 | // if you touch any of the views, increase this by one 9 | // TODO replace with crate version, always update in debug mode 10 | const VIEW_REV: i32 = 6; 11 | 12 | pub fn apply<'a, P>(db: &'a chill::Client, db_path: P) -> Result<(), Error> 13 | where P: IntoDatabasePath<'a> 14 | { 15 | let db_path = db_path.into_database_path()?; 16 | let view_id = chill::DocumentIdRef::from("_design/cloudfm"); 17 | 18 | match db.read_document((db_path, view_id))?.run() { 19 | Ok(mut doc) => { 20 | let view_doc: ViewDocument = doc.get_content()?; 21 | 22 | if view_doc.view_rev < VIEW_REV { 23 | doc.set_content(&ViewDocument::new())?; 24 | db.update_document(&doc)?.run()?; 25 | Ok(()) 26 | } else if view_doc.view_rev > VIEW_REV { 27 | Err(Error::View(ViewError::NewerRevision)) 28 | } else { 29 | Ok(()) 30 | } 31 | } 32 | Err(chill::Error::NotFound(_)) => { 33 | db.create_document(db_path, &ViewDocument::new())?.with_document_id(view_id).run()?; 34 | Ok(()) 35 | } 36 | Err(e) => Err(Error::from(e)), 37 | } 38 | } 39 | 40 | pub fn all_artists<'a, P>(db: &'a chill::Client, 41 | db_path: P) 42 | -> Result, Error> 43 | where P: IntoDatabasePath<'a> 44 | { 45 | let res = db.execute_view::, _>((db_path, "cloudfm", "all_artists"))? 46 | .run()?; 47 | let res = res.as_unreduced().ok_or(ViewError::ViewReduced)?; 48 | Ok(res.rows() 49 | .iter() 50 | .map(|a| (DocumentId::from(a.document_path().document_id()), a.key().clone())) 51 | .collect()) 52 | } 53 | 54 | pub fn all_albums<'a, P>(db: &'a chill::Client, 55 | db_path: P) 56 | -> Result, Error> 57 | where P: IntoDatabasePath<'a> 58 | { 59 | let res = db.execute_view::, _>((db_path, "cloudfm", "all_albums"))?.run()?; 60 | let res = res.as_unreduced().ok_or(ViewError::ViewReduced)?; 61 | Ok(res.rows() 62 | .iter() 63 | .map(|a| (DocumentId::from(a.document_path().document_id()), a.key().clone())) 64 | .collect()) 65 | } 66 | 67 | pub fn all_tracks<'a, P>(db: &'a chill::Client, 68 | db_path: P) 69 | -> Result, Error> 70 | where P: IntoDatabasePath<'a> 71 | { 72 | let res = db.execute_view::, _>((db_path, "cloudfm", "all_tracks"))?.run()?; 73 | let res = res.as_unreduced().ok_or(ViewError::ViewReduced)?; 74 | Ok(res.rows() 75 | .iter() 76 | .map(|a| (DocumentId::from(a.document_path().document_id()), a.key().clone())) 77 | .collect()) 78 | } 79 | 80 | // TODO make views static 81 | fn views() -> serde_json::value::Value { 82 | let mut builder = serde_json::builder::ObjectBuilder::new(); 83 | 84 | let views = vec![ 85 | View { 86 | name: "all_albums".into(), 87 | map: " 88 | function (doc) { 89 | if(doc.type == \"album\") { 90 | emit(doc.name); 91 | } 92 | } 93 | ".replace("\n", "").into(), 94 | }, 95 | View { 96 | name: "all_artists".into(), 97 | map: " 98 | function (doc) { 99 | if(doc.type == \"artist\") { 100 | emit(doc.name); 101 | } 102 | } 103 | ".replace("\n", "").into(), 104 | }, 105 | View { 106 | name: "all_tracks".into(), 107 | map: " 108 | function (doc) { 109 | if(doc.type == \"track\") { 110 | emit(doc.name); 111 | } 112 | } 113 | ".replace("\n", "").into(), 114 | }, 115 | ]; 116 | 117 | 118 | for view in views { 119 | builder = builder.insert_object(view.name.clone(), 120 | |builder| builder.insert("map", view.map.clone())); 121 | } 122 | 123 | builder.unwrap() 124 | } 125 | 126 | #[derive(Serialize, Deserialize, Debug)] 127 | pub struct ViewDocument { 128 | pub view_rev: i32, 129 | pub views: serde_json::value::Value, 130 | } 131 | 132 | impl default::Default for ViewDocument { 133 | fn default() -> Self { 134 | ViewDocument { 135 | view_rev: VIEW_REV, 136 | views: views(), 137 | } 138 | } 139 | } 140 | 141 | impl ViewDocument { 142 | pub fn new() -> Self { 143 | Self::default() 144 | } 145 | } 146 | 147 | #[derive(Serialize, Deserialize, Debug)] 148 | pub struct View { 149 | name: String, 150 | map: String, 151 | } 152 | 153 | #[derive(Debug)] 154 | pub enum ViewError { 155 | NewerRevision, 156 | ViewReduced, 157 | } 158 | 159 | impl error::Error for ViewError { 160 | fn description(&self) -> &str { 161 | match *self { 162 | ViewError::NewerRevision => "Database view revision is higher than ours", 163 | ViewError::ViewReduced => "View is reduced", 164 | } 165 | } 166 | 167 | fn cause(&self) -> Option<&error::Error> { 168 | None 169 | } 170 | } 171 | 172 | impl fmt::Display for ViewError { 173 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 174 | write!(f, "{}", error::Error::description(self)) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: node 2 | build: 3 | steps: 4 | - npm-install: 5 | cwd: app/ 6 | - npm-test: 7 | cwd: app/ 8 | --------------------------------------------------------------------------------