< Back to blog
PUBLISH DATE:
UPD:
Anton TrofimovAnton Trofimov Full Stack Software Developer

KeenEthics Experience-Sharing: How to Create a Simple Torrent Player?

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.

torrent player

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: []
        };
    }
};

There are two exported methods. The second one is used for distribution search to filter the titles that contain “mp3”. The first one is used for retrieving the list of tracks inside the distribution. Now, it merely displays the entire track path, which can be transformed to the tree view if needed. This code uses non-official rutracker API and simple page scraping, so it does not require any specific comments here.

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}</div>`);
    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

Do you have an idea for a project?

If there is something you need assistance with, we will be happy to share our experience or to help you implement the idea!