├── src ├── index.css ├── index.js ├── App.test.js ├── components │ ├── SongsList │ │ ├── SongsList.css │ │ └── SongsList.js │ └── Player │ │ ├── Player.css │ │ └── Player.js ├── App.css ├── fetchSongs.js └── App.js ├── .gitignore ├── package.json ├── specs.txt ├── README.md └── public └── index.html /src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /src/components/SongsList/SongsList.css: -------------------------------------------------------------------------------- 1 | .songs-list { 2 | padding-bottom: 9em; 3 | } 4 | 5 | .song-list-item { 6 | cursor: pointer; 7 | font-size: 1.4rem; 8 | font-weight: 300; 9 | margin: 1em 0; 10 | padding: .75em 1em; 11 | border: 1px solid #14B7F4; 12 | border-radius: 2px; 13 | color: #FFF; 14 | background-color: #171D25; 15 | } 16 | 17 | .song-list-item:hover { 18 | border-color: #9ecaed; 19 | box-shadow: 0 0 10px #9ecaed; 20 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | color: #FFF; 5 | font-family: 'Open Sans', sans-serif; 6 | background-color: #1D242E; 7 | } 8 | 9 | li { 10 | list-style: none; 11 | font-size: 1.1rem; 12 | } 13 | 14 | a { 15 | color: #FFF; 16 | text-decoration: none; 17 | } 18 | 19 | .container { 20 | width: 600px; 21 | margin: 0 auto; 22 | } 23 | 24 | .title { 25 | font-size: 3.2rem; 26 | font-weight: 300; 27 | text-align: center; 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jukebox", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "0.7.0" 7 | }, 8 | "dependencies": { 9 | "axios": "^0.15.2", 10 | "react": "^15.3.2", 11 | "react-dom": "^15.3.2" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /specs.txt: -------------------------------------------------------------------------------- 1 | v1: 2 | == 3 | Player fixed at the bottom of the screen 4 | - prev, next buttons 5 | - play-pause toggle buttons 6 | - loop song 7 | - slider 8 | - current time on left side of slider and total time on right 9 | - volume control - mute and unmute 10 | 11 | List all the songs available in a SINGLE folder for now. 12 | - clicking on a song should play it 13 | - list current song being played 14 | - play next song automatically after first finishes 15 | 16 | 17 | v2: 18 | == 19 | - search bar 20 | - deep searching inside folders? -------------------------------------------------------------------------------- /src/components/SongsList/SongsList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import './SongsList.css'; 4 | 5 | class SongsList extends Component { 6 | render() { 7 | return ( 8 |
9 | { 10 | this.props.songs.map((song, i) => { 11 | return
this.props.handleSongClick(e.target.id)}> 16 | {song.song_title.slice(0, -4)} 17 |
18 | }) 19 | } 20 |
21 | ); 22 | } 23 | } 24 | 25 | export default SongsList; -------------------------------------------------------------------------------- /src/fetchSongs.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | function filterSongs(html) { 3 | let songsList = []; 4 | 5 | let htmlDoc = document.createElement('div'); 6 | htmlDoc.innerHTML = html; 7 | 8 | let listOfLinks = htmlDoc.querySelectorAll('a'); 9 | listOfLinks = Array.from(listOfLinks); 10 | 11 | songsList = listOfLinks.map(link => { 12 | return { 13 | song_title: link.innerHTML, 14 | song_url: link.href.replace('http://localhost:3000', 'http://localhost:9000') 15 | } 16 | }); 17 | 18 | songsList = songsList.filter(link => link.song_title.includes('.mp3')); 19 | 20 | return songsList; 21 | } 22 | 23 | function fetchSongs() { 24 | return new Promise((resolve, reject) => { 25 | axios.get('http://localhost:9000') 26 | .then(response => { 27 | const songs = filterSongs(response.data); 28 | return resolve(songs); 29 | }) 30 | .catch(err => reject(err)); 31 | }); 32 | } 33 | 34 | export default fetchSongs; -------------------------------------------------------------------------------- /src/components/Player/Player.css: -------------------------------------------------------------------------------- 1 | .player-controls { 2 | position: fixed; 3 | text-align: center; 4 | bottom: 0; 5 | width: 100%; 6 | background-color: #171D25; 7 | opacity: 0.90; 8 | padding: .75em 0 1em; 9 | } 10 | 11 | .icons-container { 12 | width: 500px; 13 | height: 4rem; 14 | margin: 0 auto; 15 | } 16 | 17 | .material-icons { 18 | font-size: 2.2rem; 19 | cursor: pointer; 20 | vertical-align: middle; 21 | user-select: none; 22 | } 23 | 24 | .play, .pause { 25 | font-size: 4rem; 26 | } 27 | 28 | .repeat { 29 | float: left; 30 | margin-top: 1rem; 31 | } 32 | 33 | .glow { 34 | outline: none; 35 | border-color: #9ecaed; 36 | box-shadow: 0 0 10px #9ecaed; 37 | } 38 | 39 | .volume { 40 | float: right; 41 | margin-top: 1rem; 42 | } 43 | 44 | .slider { 45 | -webkit-appearance: none; 46 | width: 500px; 47 | height: 2px; 48 | outline: none; 49 | cursor: pointer; 50 | } 51 | 52 | .time { 53 | display: inline-block; 54 | margin: 0 .5em; 55 | vertical-align: sub; 56 | } 57 | .song-title { 58 | padding-top: .5em; 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jukebox 2 | ####Music player built in React 3 | 4 | ![jukebox screenshot](http://i.imgur.com/gXwztsK.png) 5 | 6 | ## Features 7 | 8 | * Displays list of `.mp3` files in your folder 9 | * Will play all the songs in the order displayed 10 | * You can jump to any song by clicking on it 11 | * Displays current time and total duration of the file being played 12 | * Play previous or next song 13 | * Loop through a song infinitely 14 | * Mute/unmute the volume 15 | * Highlights the song currently being played. Also lists the same at the bottom of the screen 16 | 17 | 18 | ## Setting it up on your machine 19 | 20 | ### Server 21 | - Serve files from your local folder at `http://localhost:9000/`. (I know I know! I will fix it. Have to add many more things) 22 | - You might get CORS error if you are using Python's SimpleHTTPServer. I suggest you install [http-server](https://github.com/indexzero/http-server) via npm. Then `cd` into your folder with `.mp3` files and run `http-server -a localhost -p 9000 --cors` 23 | - Right now just it will only run `.mp3` files. So just keep those in your folder. 24 | 25 | ### Client 26 | - It is bootstrapped with [create-react-app](https://github.com/facebookincubator/create-react-app) 27 | - Once your local server is up, `cd` into the project folder. Now first run `npm install` to install dependencies and then run `npm start` 28 | 29 | 30 | #### Enjoy! -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | Jukebox 18 | 19 | 20 |
21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SongsList from './components/SongsList/SongsList'; 3 | import Player from './components/Player/Player'; 4 | import fetchSongs from './fetchSongs.js'; 5 | 6 | import './App.css'; 7 | 8 | class App extends Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | songs: [], 13 | currentSong: 0 14 | } 15 | this.handlePrev = this.handlePrev.bind(this); 16 | this.handleNext = this.handleNext.bind(this); 17 | this.handleSongClick = this.handleSongClick.bind(this); 18 | } 19 | 20 | componentWillMount(){ 21 | fetchSongs() 22 | .then(songs => this.setState({songs: songs})) 23 | .catch(err => console.log(err)); 24 | } 25 | 26 | handlePrev() { 27 | // console.log("prev clicked"); 28 | const nextSong = parseInt(this.state.currentSong, 10) - 1; 29 | if(this.state.currentSong > 0) { 30 | this.setState({currentSong: nextSong}); 31 | } 32 | } 33 | 34 | handleNext() { 35 | const nextSong = parseInt(this.state.currentSong, 10) + 1; 36 | if(this.state.currentSong < this.state.songs.length - 1) { 37 | this.setState({currentSong: nextSong}); 38 | } 39 | } 40 | 41 | handleSongClick(id) { 42 | this.setState({currentSong: id}); 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 |
49 |

Jukebox

50 | 55 |
56 | { 57 | this.state.songs.length 58 | ? 64 | : null 65 | } 66 |
67 | ); 68 | } 69 | } 70 | 71 | export default App; -------------------------------------------------------------------------------- /src/components/Player/Player.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import './Player.css' 4 | 5 | class Player extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | currentSongUrl: "", 11 | currentSongTitle: "", 12 | isPlaying: false, 13 | isMute: false, 14 | currentTime: 0, 15 | duration: 0, 16 | isLooping: false 17 | } 18 | this.handlePlay = this.handlePlay.bind(this); 19 | this.handleVolume = this.handleVolume.bind(this); 20 | this.handleSlider = this.handleSlider.bind(this); 21 | this.handleLooping = this.handleLooping.bind(this); 22 | } 23 | 24 | handlePlay() { 25 | this.state.isPlaying ? this.refs.player.pause() : this.refs.player.play(); 26 | this.setState({isPlaying: !this.state.isPlaying}); 27 | } 28 | 29 | handleVolume() { 30 | this.setState({isMute: !this.state.isMute}) 31 | } 32 | 33 | handleTime() { 34 | const audio = this.refs.player; 35 | let timer; 36 | if(timer){window.cancelRequestAnimFrame(timer)}; 37 | if(audio) { 38 | timer = window.requestAnimationFrame(() => { 39 | this.setState({ 40 | currentTime: audio.currentTime, 41 | duration: audio.duration 42 | }); 43 | if(Math.floor(audio.currentTime) === Math.floor(audio.duration)) { 44 | if(this.state.isLooping) { 45 | this.refs.player.currentTime = 0; 46 | this.refs.player.play(); 47 | } else { 48 | this.props.playNext(); 49 | } 50 | } 51 | }); 52 | } 53 | } 54 | 55 | handleSlider(event) { 56 | this.refs.player.currentTime = event.target.value; 57 | this.setState({currentTime: event.target.value}); 58 | } 59 | 60 | handleLooping() { 61 | this.setState({isLooping: !this.state.isLooping}); 62 | } 63 | 64 | componentWillMount() { 65 | this.setState({ 66 | currentSongUrl: this.props.currentSongUrl, 67 | currentSongTitle: this.props.currentSongTitle 68 | }); 69 | } 70 | 71 | componentDidMount() { 72 | this.handleTime(); 73 | } 74 | 75 | componentDidUpdate() { 76 | this.handleTime(); 77 | } 78 | 79 | componentWillReceiveProps(nextProps) { 80 | // console.log("receiving ", nextProps.currentSongUrl); 81 | this.setState({ 82 | currentSongUrl: nextProps.currentSongUrl, 83 | currentSongTitle: nextProps.currentSongTitle, 84 | isPlaying: true 85 | }); 86 | } 87 | 88 | render() { 89 | return ( 90 |
91 | 92 |
93 | 94 | repeat_one 97 | 98 | 99 | skip_previous 100 | 101 | 105 | {this.state.isPlaying ? "pause_circle_outline" : "play_circle_outline"} 106 | 107 | 108 | skip_next 109 | 110 | 114 | {this.state.isMute ? "volume_off" : "volume_up"} 115 | 116 |
117 | 118 | 124 | 125 |
126 |
{convertSecondsToMinsSecs(this.state.currentTime)}
127 | 136 |
{convertSecondsToMinsSecs(this.state.duration)}
137 |
{this.state.currentSongTitle.slice(0, -4)}
138 |
139 |
140 | ); 141 | } 142 | } 143 | 144 | export default Player; 145 | 146 | function convertSecondsToMinsSecs(totalSec) { 147 | const minutes = parseInt( totalSec / 60, 10) % 60; 148 | const seconds = parseInt(totalSec % 60, 10); 149 | const result = (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds); 150 | return result; 151 | } --------------------------------------------------------------------------------