@@ -1,20 +1,6 @@ | |||
# MMPC2 | |||
The second version of Mort's Media PC. | |||
## Goal | |||
MMPC2 is meant to be running constantly on a HTPC, letting you stream media to | |||
a TV and control the playback remotely, all through a web interface. It allows | |||
you to stream three kinds of movies: | |||
* URLs - YouTube, Vimeo, Dailymotion, plain HTTP streams, anything supported by | |||
mpv and youtube-dl. | |||
* Magnet Links - Stream any torrent. | |||
* Files - Upload files and have them play. | |||
For magnet links and files, subtitles will automatically be downloaded if you | |||
want and subtitles exist for it in OpenSubtitles. | |||
Modified version of mmpc2. | |||
## Installation | |||
@@ -41,34 +27,6 @@ that. | |||
With rolling release distros, the version of node in the package manager will | |||
generally be new enough - e.g `sudo pacman -S npm node` in arch is enough. | |||
### Getting a recent version of mpv | |||
MMPC2 requires a relatively new version of mpv, newer than what's currently in | |||
Debian and Ubuntu. For Ubuntu, you could just add this ppa to your system: | |||
https://launchpad.net/~mc3man/+archive/Ubuntu/mpv-tests | |||
sudo add-apt-repository ppa:mc3man/mpv-tests | |||
sudo apt-get update | |||
sudo apt-get install mpv | |||
For Debian, you will probably have to either use the testing repositories to | |||
install mpv, get some binary from somewhere, or compile it from source. I | |||
compiled from source when installing it on the Debian stable (jessie), and the | |||
instructions are a bit too long to include here, but you can get the source | |||
from here: https://github.com/mpv-player/mpv/releases/latest and get the source | |||
there. Make sure to compile with luajit support for youtube-dl, and libpulse | |||
for pulseaudio (if you use that). | |||
### Getting a recent version of youtbe-dl | |||
Debian may ship a version of youtube-dl that's so old it doesn't really work | |||
anymore - I had that problem with my Debian box. To fix that, uninstall | |||
youtube-dl if you installed with apt-get (`apt-get remove youtube-dl`), and | |||
install it through pip (installing pip if necessary) | |||
sudo apt-get install python-pip | |||
sudo pip install youtube-dl | |||
## Configuration | |||
`conf.json` contains a couple of configuration options (assuming you copied | |||
@@ -77,13 +35,4 @@ install it through pip (installing pip if necessary) | |||
{ | |||
"tmpdir": String. The directory to store temporary files in. | |||
Default: "tmp" | |||
"subtitles": String (or false). The language code for the subtitles, | |||
or false for no subtitles. Default: "en" (english). | |||
"additional_links": Array of objects which look like this: | |||
{ "name": "some name", "url": "some URL" } | |||
Lets you add more parts to the index page. I use it to link to an | |||
instance of guacamole, a web based VNC client. | |||
( https://guacamole.incubator.apache.org/ ) | |||
} |
@@ -1,5 +1,3 @@ | |||
{ | |||
"tmpdir": "tmp", | |||
"subtitles": "en", | |||
"additional_links": [] | |||
"tmpdir": "tmp" | |||
} |
@@ -1,9 +0,0 @@ | |||
var spawn = require("child_process").spawn; | |||
module.exports = function(name, msg) { | |||
var args = ["--urgency", "low"]; | |||
args.push(name); | |||
if (msg) args.push(msg); | |||
spawn("notify-send", args); | |||
} |
@@ -4,13 +4,11 @@ var https = require("https"); | |||
var pathlib = require("path"); | |||
var urllib = require("url"); | |||
var fsutil = require("../fsutil"); | |||
var notify = require("../notify"); | |||
var player = require("./player"); | |||
var httpStream = require("./http-stream"); | |||
var torrentStreamer = require("./torrent-streamer"); | |||
var subtitleFinder = require("./subtitle-finder"); | |||
exports.httpPath = player.httpPath; | |||
exports.httpPath = "/playback"; | |||
exports.cleanupFiles = []; | |||
@@ -23,58 +21,16 @@ exports.init = function(_app, _conf) { | |||
player.init(app, conf); | |||
httpStream.init(app, conf); | |||
torrentStreamer.init(app, conf); | |||
subtitleFinder.init(app, conf); | |||
} | |||
/* | |||
* Filename is optional; in case you want to provide a filename for subtitles | |||
* but want a different path | |||
*/ | |||
exports.playFile = function(path, cb, filename) { | |||
filename = filename || pathlib.basename(path); | |||
notify("Playing file", filename); | |||
// Find file's length | |||
fs.stat(path, (err, stat) => { | |||
if (err) { | |||
console.trace(err); | |||
return cb(); | |||
} | |||
// Find subtitles | |||
subtitleFinder.find(stat.size, filename, subFile => { | |||
// Play! | |||
player.play(path, subFile, cb); | |||
}); | |||
}); | |||
} | |||
exports.playUrl = function(url, cb) { | |||
notify("Playing url...", url); | |||
// Just play, we won't bother finding subtitles | |||
player.play(url, null, cb); | |||
} | |||
exports.playTorrent = function(magnet, cb) { | |||
notify("Playing torrent..."); | |||
// Stream torrent | |||
torrentStreamer.stream(magnet, (err, filesize, filename) => { | |||
if (err) | |||
return cb(err); | |||
// Find subtitles | |||
subtitleFinder.find(filesize, filename, subFile => { | |||
// Play! | |||
notify("Starting playback.", filename); | |||
player.play(app.getAddress()+httpStream.httpPath, subFile, cb); | |||
}); | |||
cb(); | |||
}); | |||
} | |||
@@ -88,8 +44,6 @@ exports.playTorrentPage = function(url, cb) { | |||
return match[1]; | |||
} | |||
notify("Finding magnet on torrent page...", url); | |||
var urlobj = urllib.parse(url); | |||
var o = urlobj.protocol === "https:" ? https : http; | |||
o.request(urlobj, res => { | |||
@@ -98,15 +52,13 @@ exports.playTorrentPage = function(url, cb) { | |||
res | |||
.on("data", d => str += d ) | |||
.on("error", err => { | |||
notify("Error downloading page!", err.toString()); | |||
console.trace(err); | |||
cb(); | |||
}) | |||
.on("end", () => { | |||
var magnet = findMagnet(str); | |||
if (!magnet) { | |||
notify("No magnet link on page!"); | |||
cb(); | |||
cb("No magnet link on page!"); | |||
return; | |||
} | |||
@@ -1,54 +0,0 @@ | |||
var OpenSubtitles = require("opensubtitles-api"); | |||
var fs = require("fs"); | |||
var http = require("http"); | |||
var urllib = require("url"); | |||
var subs = new OpenSubtitles({ useragent: "mmpc-media-streamer" }); | |||
var conf; | |||
exports.init = function(app, _conf) { | |||
conf = _conf; | |||
} | |||
exports.find = function(filesize, filename, cb) { | |||
if (!conf.subtitles) | |||
return cb(); | |||
var subFile = conf.tmpdir+"/subs.srt"; | |||
try { | |||
fs.unlinkSync(subFile); | |||
} catch (err) {} | |||
subs.search({ | |||
sublanguageid: conf.subtitles, | |||
filesize: filesize, | |||
filename: filename | |||
}).then(subtitles => { | |||
var sub = subtitles[conf.subtitles]; | |||
if (!sub || !sub.url) | |||
return cb(); | |||
try { | |||
var ws = fs.createWriteStream(subFile); | |||
} catch (err) { | |||
console.trace(err); | |||
cb(); | |||
} | |||
http.request(urllib.parse(sub.url), res => { | |||
res.pipe(ws); | |||
res | |||
.on("error", err => { | |||
notify("Error finding subtitles", err.toString()); | |||
console.trace(err); | |||
cb(); | |||
}) | |||
.on("end", () => { | |||
ws.close(); | |||
setTimeout(() => cb(subFile), 500); | |||
}); | |||
}).end(); | |||
}); | |||
} |
@@ -1,7 +1,6 @@ | |||
var fs = require("fs"); | |||
var pathlib = require("path"); | |||
var web = require("webstuff"); | |||
var notify = require("./js/notify"); | |||
var play = require("./js/play"); | |||
var fsutil = require("./js/fsutil"); | |||
@@ -24,8 +23,13 @@ app.post("/play/url", (req, res) => { | |||
if (!fields.url) | |||
return res.redirect("/"); | |||
function cb() { | |||
res.redirect(play.httpPath); | |||
function cb(err) { | |||
if (err) { | |||
console.log(err); | |||
res.redirect("/"); | |||
} else { | |||
res.redirect(play.httpPath); | |||
} | |||
} | |||
if (fields.url.indexOf("magnet:") === 0) { | |||
@@ -33,32 +37,8 @@ app.post("/play/url", (req, res) => { | |||
} else if (fields.url.indexOf("/torrent") !== -1) { | |||
play.playTorrentPage(fields.url, cb); | |||
} else { | |||
play.playUrl(fields.url, cb); | |||
} | |||
}); | |||
}); | |||
app.post("/play/file", (req, res) => { | |||
notify("Receiving file..."); | |||
req.parseBody((err, fields, files) => { | |||
var file = files.file; | |||
if (file == null || !file.name || file.size === 0) | |||
return res.redirect("/"); | |||
var fname = conf.tmpdir+"/uploaded-file"+pathlib.extname(file.name); | |||
fsutil.move(file.path, fname, err => { | |||
if (err) { | |||
console.trace(err); | |||
return res.redirect("/"); | |||
} | |||
play.cleanupFiles.push(fname); | |||
play.playFile(fname, () => { | |||
res.redirect(play.httpPath); | |||
}, file.name); | |||
}); | |||
} | |||
}); | |||
}); | |||
@@ -13,12 +13,6 @@ | |||
<input type="url" name="url" autocomplete="off"> | |||
<button>Play</button> | |||
</form> | |||
<form class="part" method="post" enctype="multipart/form-data" action="/play/file"> | |||
<div class="name">File:</div> | |||
<input type="file" name="file" autocomplete="off"> | |||
<button>Play</button> | |||
</form> | |||
</div> | |||
<script src="/webstuff.js"></script> |
@@ -7,38 +7,9 @@ | |||
<link rel="stylesheet" href="style.css"> | |||
</head> | |||
<body> | |||
<div id="group-info"> | |||
<div id="is-playing">Not Playing</div> | |||
<div id="progress-text"></div> | |||
</div> | |||
<div id="group-bar"> | |||
<progress id="progress"></progress> | |||
</div> | |||
<div id="group-buttons"> | |||
<button id="mute">Mute</button> | |||
<button id="skip-back"><</button> | |||
<button id="pause">||</button> | |||
<button id="skip-forward">></button> | |||
<button id="exit">Exit</button> | |||
</div> | |||
<div id="group-volume"> | |||
Volume <span id="volume-text">0</span>%<br> | |||
<input id="volume" type="range"> | |||
</div> | |||
<div id="group-sub-delay"> | |||
Subtitle Delay <span id="sub-delay">0</span>s<br> | |||
<button id="sub-delay-less2">-1</button> | |||
<button id="sub-delay-less">-0.1</button> | |||
<button id="sub-delay-reset">0</button> | |||
<button id="sub-delay-more">+0.1</button> | |||
<button id="sub-delay-more2">+1</button> | |||
</div> | |||
<script src="/webstuff.js"></script> | |||
<script src="script.js"></script> | |||
<video controls autoplay> | |||
<source src="/http-stream"> | |||
Your browser does not support the video tag. | |||
</video> | |||
</body> | |||
</html> |
@@ -1,165 +0,0 @@ | |||
function timeformat(sec) { | |||
var d = new Date(null); | |||
d.setSeconds(Math.floor(sec)) | |||
return d.toISOString().substr(11, 8); | |||
} | |||
var elems = { | |||
is_playing: $$("#is-playing"), | |||
progress_text: $$("#progress-text"), | |||
progress: $$("#progress"), | |||
pause: $$("#pause"), | |||
skip_back: $$("#skip-back"), | |||
skip_forward: $$("#skip-forward"), | |||
mute: $$("#mute"), | |||
exit: $$("#exit"), | |||
volume: $$("#volume"), | |||
volume_text: $$("#volume-text"), | |||
sub_delay: $$("#sub-delay"), | |||
sub_delay_less: $$("#sub-delay-less"), | |||
sub_delay_less2: $$("#sub-delay-less2"), | |||
sub_delay_more: $$("#sub-delay-more"), | |||
sub_delay_more2: $$("#sub-delay-more2"), | |||
sub_delay_reset: $$("#sub-delay-reset") | |||
}; | |||
var state = {}; | |||
/* | |||
* Update GUI stuff | |||
*/ | |||
function update(state) { | |||
// Playing | |||
if (state.playing) | |||
elems.is_playing.innerHTML = "Playing"; | |||
else | |||
location.href = "/"; | |||
// Progress text | |||
elems.progress_text.innerHTML = | |||
timeformat(state.time_pos)+"/"+ | |||
timeformat(state.duration); | |||
// Progress bar | |||
elems.progress.value = state.time_pos; | |||
elems.progress.max = state.duration; | |||
// Buttons | |||
elems.pause.className = state.paused ? "active" : ""; | |||
elems.mute.className = state.muted ? "active" : ""; | |||
// Volume | |||
elems.volume.min = 0; | |||
elems.volume.max = state.volume_max; | |||
elems.volume.value = state.volume; | |||
elems.volume.step = 5; | |||
elems.volume_text.innerHTML = state.volume; | |||
// Sub Delay | |||
elems.sub_delay.innerHTML = state.sub_delay; | |||
} | |||
function checkState() { | |||
post("/playback/state", null, function(err, res) { | |||
if (err) | |||
return; | |||
state = JSON.parse(res); | |||
update(state); | |||
}); | |||
} | |||
checkState(); | |||
setInterval(checkState, 500); | |||
/* | |||
* React to input | |||
*/ | |||
function playerset(key, val) { | |||
post("/playback/set/"+key+"/"+val, null, function() { | |||
checkState(); | |||
}); | |||
} | |||
// Set time | |||
elems.progress.addEventListener("click", function(evt) { | |||
var pos = (evt.clientX - evt.target.offsetLeft) / evt.target.clientWidth; | |||
pos *= evt.target.max; | |||
playerset("time-pos", pos); | |||
}); | |||
// Toggle pause | |||
elems.pause.addEventListener("click", function() { | |||
if (state.paused) { | |||
playerset("pause", "no"); | |||
} else { | |||
playerset("pause", "yes"); | |||
playerset("time-pos", state.time_pos - 1); | |||
} | |||
}); | |||
// Back 15 seconds | |||
elems.skip_back.addEventListener("click", function() { | |||
playerset("time-pos", state.time_pos - 15); | |||
}); | |||
// Forwards 15 seconds | |||
elems.skip_forward.addEventListener("click", function() { | |||
playerset("time-pos", state.time_pos + 15); | |||
}); | |||
// Toggle mute | |||
elems.mute.addEventListener("click", function(evt) { | |||
if (state.muted) | |||
playerset("mute", "no"); | |||
else | |||
playerset("mute", "yes"); | |||
}); | |||
// Exit | |||
elems.exit.addEventListener("click", function(evt) { | |||
post("/playback/exit", null, function() {}); | |||
}); | |||
// Set volume | |||
elems.volume.addEventListener("change", function(evt) { | |||
playerset("volume", evt.target.value); | |||
}); | |||
elems.volume.addEventListener("keydown", function(evt) { | |||
if (evt.keyCode === 37 || evt.keyCode === 40) | |||
playerset("volume", parseInt(evt.target.value) - parseInt(evt.target.step)); | |||
else if (evt.keyCode === 38 || evt.keyCode === 39) | |||
playerset("volume", parseInt(evt.target.value) + parseInt(evt.target.step)); | |||
}); | |||
// Less subtitle delay | |||
elems.sub_delay_less.addEventListener("click", function() { | |||
playerset("sub-delay", state.sub_delay - 0.1); | |||
}); | |||
elems.sub_delay_less2.addEventListener("click", function() { | |||
playerset("sub-delay", state.sub_delay - 1); | |||
}); | |||
// More subtitle delay | |||
elems.sub_delay_more.addEventListener("click", function() { | |||
playerset("sub-delay", state.sub_delay + 0.1); | |||
}); | |||
elems.sub_delay_more2.addEventListener("click", function() { | |||
playerset("sub-delay", state.sub_delay + 1); | |||
}); | |||
// Set subtitle delay to 0 | |||
elems.sub_delay_reset.addEventListener("click", function() { | |||
playerset("sub-delay", 0); | |||
}); | |||
window.addEventListener("keydown", function(evt) { | |||
console.log(evt.keyCode); | |||
}); |
@@ -1,89 +1,7 @@ | |||
* { | |||
box-sizing: border-box; | |||
line-height: 30px; | |||
font-family: Sans-Serif | |||
} | |||
body { | |||
margin: 12px; | |||
} | |||
body > div { | |||
margin-bottom: 12px; | |||
} | |||
button { | |||
display: inline-block; | |||
padding: 0px; | |||
border: 1px solid #666; | |||
width: 50px; | |||
height: 32px; | |||
background: #ddd; | |||
margin-left: 2px; | |||
margin-right: 2px; | |||
} | |||
button:active, | |||
button.active { | |||
background: #aaa; | |||
} | |||
/* | |||
* Playback | |||
*/ | |||
#is-playing, | |||
#progress-text { | |||
display: inline-block; | |||
} | |||
#progress-text { | |||
float: right; | |||
} | |||
#group-buttons { | |||
text-align: center; | |||
} | |||
progress { | |||
width: 100%; | |||
height: 24px; | |||
} | |||
/* | |||
* Volume | |||
*/ | |||
input[type="range"] { | |||
border: 1px solid rgba(0, 0, 0, 0); | |||
box-sizing: border-box; | |||
margin: 0px; | |||
width: 100%; | |||
} | |||
#group-volume { | |||
text-align: center; | |||
border-top: 1px solid #ccc; | |||
} | |||
#group-volume { | |||
padding-top: 12px; | |||
margin-top: 24px; | |||
} | |||
/* | |||
* Sub Delay | |||
*/ | |||
#group-sub-delay { | |||
text-align: center; | |||
} | |||
#group-sub-delay { | |||
border-top: 1px solid #ccc; | |||
} | |||
#group-sub-delay { | |||
padding-top: 12px; | |||
} | |||
#group-sub-delay button { | |||
margin-top: 6px; | |||
video { | |||
width: 100%; | |||
} |