Have you ever heard about PopcornTime? This app allows you to play a video from BitTorrent without downloading the whole file. Why not develop a similar thing but for audio? It is a good opportunity to practice and to learn some Node.js basics.
Here, I will show only how to work with mp3, but you may easily reimplement other formats using libraries. As a source tracker, I will use rutracker. I have tried to use PirateBay, but it does not have a way to get tracks path without taking torrent info, which is useful to play a single track faster.
I use Electron since it is simpler, but you can easily take it to usual Node.js + simple static server. I have three main js scripts: rutracker search wrapper, client code, and server-side code. The code is not perfect, and it is very simplified for POC.
Rutracker.js: Rutracker Search Wrapper
const torrentStream = require("torrent-stream");
const RutrackerAPI = require("rutracker-api-2");
const pRequest = require("request-promise");
const cheerio = require("cheerio");
const rutracker = new RutrackerAPI();
let trackerCookie;
const RUTRACKER_LOGIN = "searchertrack",
RUTRACKER_PASSWORD = "1q1Q1q1Q";
(async () => {
trackerCookie = await rutracker.login(RUTRACKER_LOGIN, RUTRACKER_PASSWORD);
})();
const getTrackerMP3List = (html, magnetHTML) => { const $ = cheerio.load(html);
const $m = cheerio.load(magnetHTML);
const elem = $(".ftree");
const magnet = $m('[class*="magnet-link-"]').attr('href') const result = Array.from($elem.find('b:contains(.mp3)')).map((x, i) => { const title = $(x).text(); const $dir = $(x).parents('.dir');
const pathParts = Array.from($dir).map(x => $(x).find('>div b').text().replace('./', '')).reverse();
pathParts.push(title);
const resultPath = pathParts.join(' || ');
return { id: ${Date.now()}${i}, trackPath: resultPath, shortname: title, title: resultPath, isTracker: true, magnet, };
});
return result;
};
module.exports = { TrackerFiles: async (url) => { const id = url.replace('http://rutracker.org/forum/viewtopic.php?t=', '');
const magnetHTML = await pRequest(url, { headers: { cookie: trackerCookie, }, });
const html = await pRequest('https://rutracker.org/forum/viewtorrent.php', { method: "POST", headers: { cookie: trackerCookie, }, form: { t: id } });
const result = getTrackerMP3List(html, magnetHTML);
return result;
}, Search: async (str) => { const data = await rutracker.search(str, 'size', false);
return { playlist: data .filter(x => x.title.toLowerCase().indexOf('mp3') > -1) .map(x => ({ permalink_url: x.url, title: x.title || "NO_TITLE", id: x.id, type: 'tracker', })), user: [], track: [] };
} };
Serv.js: Server-Side Code
const torrentStream = require("torrent-stream");
const fastify = require("fastify")();
const util = require('util');
module.exports = (port) => { let engine, track;
fastify.get('/trackerStream', async (request, response) => { const { trackPath, magnet } = request.query;
const filePathTorr = trackPath.split(' || ').join('\'); const filePathTorrLinux = trackPath.split(' || ').join('/');
if (engine) engine.destroy();
engine = torrentStream(magnet);
engine.on('error', (err) => console.log(err));
await util.promisify(engine.on('ready'));
track = engine.files.find(x => x.path === filePathTorr || x.path === filePathTorrLinux);
const total = track.length; const range = request.headers.range;
if (range) { const parts = range.replace(/bytes=/, "").split("-");
const partialstart = parts[0];
const partialend = parts[1];
const start = parseInt(partialstart, 10);
const end = partialend ? parseInt(partialend, 10) : total - 1;
const chunksize = (end - start) + 1;
response .code(206) .header('Content-Range', 'bytes ' + start + '-' + end + '/' + total) .header('Accept-Ranges', 'bytes') .header('Content-Length', chunksize) .header('Content-Type', 'audio/mpeg') .send(track .createReadStream({ start, end }) .on('end', () => { console.log('Downloaded');
engine.destroy(); }));
} else { response .header('Content-Type', 'audio/mpeg') .send(track.createReadStream());
} }) const start = async () => { try { await fastify.listen(port);
console.log(server listening on ${fastify.server.address().port});
console.log(server listening on ${fastify.server.address().port});
} catch (err) { console.error(err);
} } start() } }
I use fastify as a server framework. It exposes only one route, which is used for streaming torrent to audio tag. At the same time, it has 2 limitations.
Firstly, web-torrent does not support TCP/UDP in browser, while Node.js development does. Respectively, I can only use torrents that have seeders based on WebRTC fallback. In case I want to get magnet url from tracker and just play i, it will not work.
Secondly, I just choose a simpler way to pipe Node.js readable Stream to browser audio. I have found an alternative way, but it does not seem to support track seeking.
Let’s see what the code does.
I select a track on the client side and pass trackPath argument to the server. It contains the full path to a specific track including the directory structure.
Then, I get torrent info by magnet, find a specific file inside, take read stream, and pipe it to response. This url is used as file inside audio tag src. A good thing is that createReadStream supports ranges, so it is easy to implement audio seeking.
Main.js: Client Code
const RuTracker = require('./js/rutracker.js');
const fs = require('fs');
const port = 3007;
require('./js/serv.js')(port);
document .querySelector('#searchSubmit') .addEventListener('click', async () => { const searchStr = document.querySelector('#searchStr').value.toLowerCase();
try { const result = await RuTracker.Search(searchStr);
const items = result .playlist .map(x => <div class="searchItem" onClick="listTracks('${x.permalink_url}')">${x.title}</div>);
document.querySelector('#searchOutput').innerHTML = items.join('');
} catch (ex) { console.error(ex); }
});
const listTracks = async (url) => { const tracks = await RuTracker.TrackerFiles(url); const items = tracks.map(x => <div class="searchItem" onClick="playTrack(\${x.trackPath}`, '${x.magnet}')">${x.title});document.querySelector('#searchOutput').innerHTML = items.join('');console.log(tracks);} const playTrack = async (trackPath, magnet) => { document.querySelector('#audioOutput').src =http://localhost:${port}/trackerStream?trackPath=${trackPath}&magnet=${encodeURIComponent(magnet)}`; }
This file can be used for a simple search and audio output.
To Wrap up
I have provided an example of how easy it can be to create a torrent player of some kind. Sure, it has some issues, such as the search sometimes does not work or huge files are opened slowly. Yet, it is just a POC, and most of these problems can be solved.
You can find code here: https://github.com/demogoran/ElectroPlay
If there is something you need assistance with, we will be happy to share our experience or to help you implement the idea!