@@ -1,2 +1,3 @@ | |||
node_modules | |||
conf.json | |||
tmp |
@@ -0,0 +1,89 @@ | |||
# 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. | |||
## Installation | |||
Install git, a recent version of node, and npm, clone the repository, run `npm | |||
install`, copy `conf.json.example` to `conf.json`, and run `node server.js`. | |||
git clone https://github.com/mortie/mmpc2.git | |||
cd mmpc2 | |||
npm install | |||
cp conf.json.example | |||
node server.js | |||
### Getting a recent version of node.js | |||
Many distros ship old versions of node, which won't work with MMPC2. To fix | |||
this, install node.js and npm (`sudo apt-get install nodejs-legacy npm` on | |||
Debian and Ubuntu), then install `n` and install a new version of node with | |||
that. | |||
sudo apt-get install nodejs-legacy npm | |||
sudo npm install -g n | |||
sudo n stable | |||
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 | |||
`conf.json.example` to `conf.json`). These are: | |||
{ | |||
"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,4 +1,5 @@ | |||
{ | |||
"tmpdir": "tmp", | |||
"subtitles": "en" | |||
"subtitles": "en", | |||
"additional_links": [] | |||
} |
@@ -0,0 +1,50 @@ | |||
var fs = require("fs"); | |||
var pathlib = require("path"); | |||
/* | |||
* Move a file by copying it, to let it move across devices | |||
*/ | |||
exports.move = function(src, dst, cb) { | |||
var ws; | |||
try { | |||
ws = fs.createWriteStream(dst); | |||
} catch (err) { return cb(err) } | |||
var rs; | |||
try { | |||
rs = fs.createReadStream(src); | |||
} catch (err) { return cb(err) } | |||
rs | |||
.on("data", d => ws.write(d)) | |||
.on("end", () => { | |||
ws.end(); | |||
cb(); | |||
}) | |||
.on("error", cb); | |||
} | |||
/* | |||
* Remove directory, deleting its content in the process | |||
*/ | |||
exports.rmdir = function(dir) { | |||
console.log("rmdir", dir); | |||
try { | |||
fs.accessSync(dir, fs.F_OK) | |||
} catch (err) { | |||
console.trace(err); | |||
return; | |||
} | |||
fs.readdirSync(dir).forEach(f => { | |||
var fname = pathlib.join(dir, f); | |||
var stat = fs.statSync(fname); | |||
if (stat.isDirectory()) | |||
exports.rmdir(fname); | |||
else | |||
fs.unlinkSync(fname) | |||
}); | |||
fs.rmdir(dir); | |||
} |
@@ -1,5 +1,6 @@ | |||
var fs = require("fs"); | |||
var pathlib = require("path"); | |||
var fsutil = require("../fsutil"); | |||
var player = require("./player"); | |||
var httpStream = require("./http-stream"); | |||
var torrentStreamer = require("./torrent-streamer"); | |||
@@ -7,23 +8,37 @@ var subtitleFinder = require("./subtitle-finder"); | |||
exports.httpPath = player.httpPath; | |||
exports.cleanupFiles = []; | |||
var app; | |||
var conf | |||
exports.init = function(_app, conf) { | |||
exports.init = function(_app, _conf) { | |||
app = _app; | |||
conf = _conf; | |||
player.init(app, conf); | |||
httpStream.init(app, conf); | |||
torrentStreamer.init(app, conf); | |||
subtitleFinder.init(app, conf); | |||
} | |||
exports.playFile = function(path, cb) { | |||
/* | |||
* 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); | |||
// Find file's length | |||
fs.stat(path, (err, stat) => { | |||
if (err) { | |||
console.trace(err); | |||
return cb(); | |||
} | |||
// Find subtitles | |||
subtitleFinder.find(stat.size, pathlib.basename(path), subFile => { | |||
subtitleFinder.find(stat.size, filename, subFile => { | |||
exports.cleanupFiles.push(subFile); | |||
// Play! | |||
player.play(path, subFile, cb); | |||
@@ -46,6 +61,7 @@ exports.playTorrent = function(magnet, cb) { | |||
// Find subtitles | |||
subtitleFinder.find(filesize, filename, subFile => { | |||
exports.cleanupFiles.push(subFile); | |||
// Play! | |||
player.play(app.getAddress()+httpStream.httpPath, subFile, cb); | |||
@@ -58,4 +74,11 @@ exports.isPlaying = player.isPlaying; | |||
player.onstop = function() { | |||
torrentStreamer.stop(); | |||
httpStream.stop(); | |||
exports.cleanupFiles.forEach(f => { | |||
fs.unlink(f, err => { if (err) console.trace(err) }); | |||
}); | |||
exports.cleanupFiles = []; | |||
fsutil.rmdir(conf.tmpdir+"/torrent-stream"); | |||
} |
@@ -90,7 +90,7 @@ exports.play = function(path, subFile, cb) { | |||
path, | |||
"--no-cache-pause", | |||
"--no-resume-playback", | |||
"--input-unix-socket", ipcServer | |||
"--input-ipc-server", ipcServer | |||
]; | |||
if (subFile) { |
@@ -1,6 +1,8 @@ | |||
var fs = require("fs"); | |||
var pathlib = require("path"); | |||
var web = require("webstuff"); | |||
var play = require("./js/play"); | |||
var fsutil = require("./js/fsutil"); | |||
var conf = JSON.parse(fs.readFileSync("conf.json")); | |||
@@ -37,3 +39,30 @@ app.post("/play/magnet", (req, res) => { | |||
}); | |||
}); | |||
}); | |||
app.post("/play/file", (req, res) => { | |||
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); | |||
}); | |||
}); | |||
}); | |||
app.get("/additional-links", (req, res) => { | |||
res.json(conf.additional_links); | |||
}); |
@@ -19,8 +19,15 @@ | |||
<input type="url" name="magnet" 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> | |||
<script src="/script.js"></script> | |||
</body> | |||
</html> |
@@ -0,0 +1,18 @@ | |||
get("/additional-links", function(err, res) { | |||
if (err) | |||
return console.trace(err); | |||
JSON.parse(res).forEach(function(link) { | |||
var form = document.createElement("form"); | |||
form.className = "part"; | |||
form.method = "get"; | |||
form.action = link.url; | |||
var btn = document.createElement("button"); | |||
btn.className = "link"; | |||
btn.innerHTML = link.name; | |||
form.appendChild(btn); | |||
$$("#parts").appendChild(form); | |||
}); | |||
}); |
@@ -21,12 +21,24 @@ | |||
#parts .part input { | |||
width: calc(100% - 80px); | |||
} | |||
#parts .part input[type=url], | |||
#parts .part input[type=text] { | |||
padding-left: 6px; | |||
} | |||
#parts .part button { | |||
width: 75px; | |||
float: right; | |||
} | |||
#parts .part button.link { | |||
width: 100%; | |||
height: 42px; | |||
float: none; | |||
} | |||
#parts .part input, | |||
#parts .part button { | |||
height: 28px; | |||
height: 34px; | |||
line-height: 0px; | |||
} |