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}); } 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!