Full Code of demaisj/soundcloud-rp for AI

master 71a667f1e5ea cached
17 files
24.8 KB
6.5k tokens
23 symbols
1 requests
Download .txt
Repository: demaisj/soundcloud-rp
Branch: master
Commit: 71a667f1e5ea
Files: 17
Total size: 24.8 KB

Directory structure:
gitextract_wq4eimy9/

├── .gitignore
├── README.md
├── bin/
│   └── soundcloud-rp
├── config/
│   └── default.json
├── package.json
├── soundcloud-rp.sublime-project
├── soundcloud-rp.user.js
└── src/
    ├── helpers/
    │   ├── artwork.js
    │   ├── discord.js
    │   ├── image.js
    │   └── soundcloud.js
    ├── index.js
    ├── procedures/
    │   └── activity.js
    ├── protocols/
    │   └── client.js
    ├── routes/
    │   └── client.js
    ├── rpc.js
    └── views/
        └── client.hbs

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================

# Created by https://www.gitignore.io/api/node,linux,windows,sublimetext

### Linux ###
*~

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*

### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env


### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache

# workspace files are user-specific
*.sublime-workspace

# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project

# sftp configuration file
sftp-config.json

# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache

# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings

### Windows ###
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msm
*.msp

# Windows shortcuts
*.lnk


# End of https://www.gitignore.io/api/node,linux,windows,sublimetext

misc/

================================================
FILE: README.md
================================================
<img src="assets/default.png?raw=true" width="128" height="128" align="left">
<h1>Soundcloud Rich Presence</h1>
Adds Discord Rich Presence support to Soundcloud.
<br><br>

## Introduction

Soundcloud Rich Presence allows you to show off your Soundcloud listening session to your friends using Discord Rich Presence. 

It is a combination of a server, communicating with discord itself, and a user-script, running on your browser to send playback information to the server. Sadly, due to restrictions in the rich presence protocol, it is mandatory to run both the server and the user-script in order for the system to work.

Artwork upload is not available by default due to Discord's asset limit (150). In order to activate it, you need to create a new app on the developer interface, and set the new ClientID and your APIKey of the developer interface. More details at **[Artwork Upload](#artwork-upload)**.

## Preview

### With artwork upload

| Profile | Popup |
| ------ | ----- |
| ![](doc/preview-artwork-profile.png?raw=true) | ![](doc/preview-artwork-popup.png?raw=true) |

### Without artwork upload

| Profile | Popup |
| ------ | ----- |
| ![](doc/preview-no-artwork-profile.png?raw=true) | ![](doc/preview-no-artwork-popup.png?raw=true) |

## Installation

You will need to install [nodejs (v10) and npm (v6)](https://nodejs.org/en/download/current/) first. Make sure the `node` & `npm` commands are installed on your **PATH**.

**Server:**
1. Clone the repository somewhere on your hard drive or [unzip this archive](https://github.com/demaisj/soundcloud-rp/archive/master.zip) if you don't have git installed
2. Open a terminal in the **soundcloud-rp** directory
3. Install the dependencies with `npm install`
4. Retrieve your Soundcloud ClientID :
   - Open [Soundcloud](https://soundcloud.com/) then hit Ctrl+Shift+I to open the devtools
   - Go to the **Network** tab
   - Filter by `api-v2.soundcloud.com`
   - Click on the first result. If there is no results, try changing page on Soundcloud to trigger some requests
   - Scroll down to the **Query String Parameters** section
   - Look for the **client_id** field and copy the value
   - Paste it in the corresponding field of the `config/default.json` file
5. Start the server with `npm run start`
6. Additionnaly create a systemd service (linux) or startup shortcut (windows) to start the server on bootup

**Browser:**
1. Install a userscript extension for your browser like [Tampermonkey](https://tampermonkey.net/)
2. Download & install [`soundcloud-rp.user.js`](soundcloud-rp.user.js?raw=true)
3. Open soundcloud & enjoy

## Artwork upload

Here is a step by step guide to activate artwork upload:
1. In the `config/default.json` file, change `uploadArtwork` from `false` to `true`
2. Go to the [developer interface](https://discordapp.com/developers/applications/me) of Discord
3. Create a new app, give it a cool name and save it
4. Paste the Client ID (found in App Details on the top of the page) into the `config/default.json` file
5. ~~Scroll down and click "*Enable Rich Presence*"~~
6. Hit save changes just in case
7. Retrieve your APIKey
   - Hit ctrl+shift+i to open the devtools
   - Go to the **Network** tab
   - Filter by `/api/`
   - Click on the first result. If there is no results, try changing page to trigger some requests
   - Scroll down to the **Request Headers** section
   - Look for the **authorization** field and copy the value
   - Paste it in the corresponding field of the `config/default.json` file (do not forget to wrap it in double quotes as in `"value"`)
8. Restart your server and it should be ok!


================================================
FILE: bin/soundcloud-rp
================================================
#!/usr/bin/env node

/**
 * Module dependencies.
 */

var server = require('../src/');
var debug = require('debug')('soundcloud-rp:server');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '7769');

/**
 * Listen on provided port, locally
 */

server.listen(port, "127.0.0.1");
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}


================================================
FILE: config/default.json
================================================
{
  "discord": {
    "ClientID": "443074120758853643",
    "APIKey": null
  },
  "soundcloud": {
    "ClientID": "{insert soundcloud client_id}"
  },
  "uploadArtwork": false
}

================================================
FILE: package.json
================================================
{
  "name": "soundcloud-rp",
  "version": "2.0.3",
  "description": "Adds Discord Rich Presence support to Soundcloud.",
  "main": "src/index.js",
  "dependencies": {
    "cors": "^2.8.4",
    "datauri": "^1.1.0",
    "discord-rpc": "^3.0.0",
    "express": "^4.16.3",
    "hbs": "^4.0.1",
    "request": "^2.87.0",
    "request-promise-native": "^1.0.5",
    "socket.io": "^2.1.1"
  },
  "devDependencies": {
    "nodemon": "^1.18.3"
  },
  "scripts": {
    "start": "node bin/soundcloud-rp",
    "watch": "nodemon --watch src/ --watch config/ --exec npm start",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "demaisj",
  "license": "ISC"
}


================================================
FILE: soundcloud-rp.sublime-project
================================================
{
	"folders":
	[
		{
			"path": "."
		}
	]
}


================================================
FILE: soundcloud-rp.user.js
================================================
// ==UserScript==
// @name         Soundcloud Rich Presence
// @namespace    https://github.com/demaisj/soundcloud-rp
// @version      2.0.0
// @description  Adds Discord Rich Presence support to Soundcloud. A server is needed to run in background in order for the system to work.
// @author       demaisj
// @match        https://soundcloud.com/*
// @grant        none
// ==/UserScript==

(function(){
  function load_script() {
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.id = 'soundcloud-rp-client';
    var match = document.getElementById(script.id);
    if (match)
      match.remove();
    script.type = 'text/javascript';
    script.src = 'http://127.0.0.1:7769/client.js';
    script.onerror = function(){
      setTimeout(load_script, 10 * 1000);
    };
    head.appendChild(script);
  }
  load_script();
})();

================================================
FILE: src/helpers/artwork.js
================================================
module.exports = {
  MAX_ARTWORK: 150,
  ARTWORK_TRACK: 1,
  ARTWORK_ARTIST: 2
};

================================================
FILE: src/helpers/discord.js
================================================
const request = require('request-promise-native');
const trace = require('debug')('soundcloud-rp:trace');

module.exports = (config) => {

  function getAssetList() {
    trace("discord.getAssetList");

    return request.get(`https://discordapp.com/api/oauth2/applications/${config.discord.ClientID}/assets`, {
      headers: {
        authorization: config.discord.APIKey
      },
      json: true
    });
  }

  function uploadAsset(type, key, data) {
    trace('discord.uploadAsset', type, key);

    return request.post(`https://discordapp.com/api/oauth2/applications/${config.discord.ClientID}/assets`, {
      headers: {
        authorization: config.discord.APIKey
      },
      json: true,
      body: {
        name: key,
        type: type,
        image: data
      }
    })
  }

  function deleteAsset(id) {
    trace('discord.deleteAsset', id);

    return request.delete(`https://discordapp.com/api/oauth2/applications/${config.discord.ClientID}/assets/${id}`, {
      headers: {
        authorization: config.discord.APIKey
      }
    })
  }

  return {
    getAssetList,
    uploadAsset,
    deleteAsset
  };
}

================================================
FILE: src/helpers/image.js
================================================
const request = require('request-promise-native');
const trace = require('debug')('soundcloud-rp:trace');
const DataURI = require('datauri');
const url_parser = require('url').parse;
const path = require('path');

function imageDataFromFile(pathname) {
  trace('image.imageDataFromFile', pathname);

  const datauri = new DataURI();

  return new Promise((resolve, reject) => {
    datauri
      .on('encoded', resolve)
      .on('error', reject)
      .encode(pathname)
  });
}

function imageDataFromUrl(url) {
  trace('image.imageDataFromUrl', url);

  const datauri = new DataURI();

  return new Promise((resolve, reject) => {

    request.get(url, {
      encoding: null
    })
    .then((buffer) => {
      let parsed = url_parser(url),
        filename = path.basename(parsed.pathname);

      datauri.format(filename, buffer);
      resolve(datauri.content);
    })
    .catch(reject);
  })
}

module.exports = {
  imageDataFromUrl,
  imageDataFromFile
};

================================================
FILE: src/helpers/soundcloud.js
================================================
const request = require('request-promise-native');
const trace = require('debug')('soundcloud-rp:trace');

module.exports = (config) => {

  function getTrackData(url) {
    trace('soundcloud.getTrackData', url);

    return request.get('https://api-v2.soundcloud.com/resolve', {
      qs: {
        client_id: config.soundcloud.ClientID,
        url
      },
      json: true
    });
  }

  function sanitizeArtworkUrl(url) {
    trace('soundcloud.sanitizeArtworkUrl', url);

    return url.replace('large', 't500x500');
  }

  return {
    getTrackData,
    sanitizeArtworkUrl
  };
}

================================================
FILE: src/index.js
================================================
const express = require('express');
const http = require('http');
const socketio = require('socket.io')
const path = require('path');
const bodyParser = require('body-parser');
const debug = require('debug')('soundcloud-rp:server');
const cors = require('cors');

const config = require('../config/default.json');

if (config.soundcloud.ClientID == '{insert soundcloud client_id}')
  throw new Error('Please edit the default soundcloud client_id before starting the server');

const rpc = require('./rpc')(config);


const app = express();
const server = http.createServer(app);
const io = socketio(server);

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');

const client_routes = require('./routes/client');

app.use(client_routes);

const client_protocol = require('./protocols/client')(config, io, rpc);

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  let data = {
    code: err.status || err.statusCode || 500,
    error: err.name,
    message: err.message
  };

  if (req.app.get('env') !== 'production') {
    data.debug = err;
    data.stack = err.stack;
  }

  // render the error page
  debug(`${err.stack}`);
  res.status(data.code);
  res.json(data);
});


module.exports = server;

================================================
FILE: src/procedures/activity.js
================================================
const trace = require('debug')('soundcloud-rp:trace');
const debug = require('debug')('soundcloud-rp:activity');
const { MAX_ARTWORK, ARTWORK_TRACK, ARTWORK_ARTIST } = require('../helpers/artwork');
const image = require('../helpers/image');

const WAIT_BEFORE_CLEAR = 15;

module.exports = (config, rpc) => {

  const soundcloud = require('../helpers/soundcloud')(config);
  const discord = require('../helpers/discord')(config);

  function processArtwork(type, id, url) {
    trace('activity.processArtwork', type, id, url);

    return new Promise((resolve, reject) => {
      const key = `${type == ARTWORK_TRACK ? 'track' : 'artist'}_${id}`;

      debug("Generated key for artwork:", key);

      if (!config.uploadArtwork) {
        debug("Uploading artworks is not supported, falling back to default");

        if (config.discord.ClientID == '443074120758853643') {
          if (type == ARTWORK_TRACK)
            return resolve(`default_large_${id % 11}`);
          else if (type == ARTWORK_ARTIST)
            return resolve('default_small');
        }
        return resolve('default');
      }

      debug("Checking if artwork is already uploaded...");
      discord.getAssetList()
      .then((assets) => {

        for (let i = 0; i < assets.length; i++) {
          if (assets[i].name == key) {
            debug("Artwork already uploaded, no action needed.");
            return resolve(key);
          }
        }

        debug("Artwork not already uploaded.");

        function continueUpload() {
          let image_processor;

          if (url == null || url.startsWith('http://a1.sndcdn.com/images/default_avatar_large.png')) {
            debug("Artwork is placeholder, getting datauri from stock ones...");
            image_processor = image.imageDataFromFile(`assets/placeholder-${id % 11}.png`);
          }
          else {
            debug("Getting artwork datauri from soundcloud cdn...");
            image_processor = image.imageDataFromUrl(soundcloud.sanitizeArtworkUrl(url));
          }

          image_processor
          .then((data) => {
            debug("Uploading artwork to discord...");
            discord.uploadAsset(type, key, data)
            .then(() => {
              debug("Artwork processed successfully!");
              resolve(key);
            })
            .catch(reject)
          })
          .catch(reject)
        }

        if (assets.length >= MAX_ARTWORK) {
          debug("Asset limit reached, deleting old unused assets...");
          discord.deleteAsset(assets[0].id)
          .then(continueUpload)
          .catch(reject);
        } else {
          continueUpload();
        }

      })
      .catch(reject)
    });
  }

  let LOCKED = false;

  return (request_data) => {
    trace('activity', request_data);

    return new Promise((resolve, reject) => {

    if (!('url' in request_data) || !('pos' in request_data)) {
      debug("Bad Request, missing arguments");
      reject(new Error('Missing url/pos argument.'));
      return;
    }

    if (!rpc.status) {
      debug("Service Unavailable, rpc not connected");
      reject(new Error('RPC not connected to Discord.'));
      return;
    }

    if (LOCKED) {
      debug("LOCKED state, we are already updating activity");
      reject(new Error('An activity request is already being processed.'));
      return;
    }

    function success() {
      LOCKED = false;
      resolve();
    }

    function error(err) {
      LOCKED = false;
      reject(err);
    }

    try{

    LOCKED = true;

    let last_activity = rpc.getActivity();
    if (last_activity && last_activity.trackURL == request_data.url) {
      debug('track info already sent, updating timestamps only...');
      last_activity.startTimestamp = Math.round(new Date().getTime() / 1000) - request_data.pos;
      last_activity.endTimestamp = last_activity.startTimestamp + Math.round(last_activity.trackDuration / 1000);

      rpc.setActivity(last_activity)
      .then(() => {
        rpc.setActivityTimeout(last_activity.endTimestamp + WAIT_BEFORE_CLEAR);

        success();
      })
      .catch(error);
      return;
    }

    debug("getting track info...");
    soundcloud.getTrackData(request_data.url)
    .then((track_data) => {
      debug("Track info downloaded successfully.", track_data.id);

      let startTimestamp = Math.round(new Date().getTime() / 1000) - request_data.pos,
        endTimestamp = startTimestamp + Math.round(track_data.duration / 1000);

      debug("Processing artwork...");
      let keys = [];

      processArtwork(ARTWORK_TRACK, track_data.id, track_data.artwork_url)
      .then((key) => keys.push(key))
      .then(() => processArtwork(ARTWORK_ARTIST, track_data.user.id, track_data.user.avatar_url))
      .then((key) => keys.push(key))
      .then(() => {
        debug('Artwork processed successfully', keys);

        let activity_data = {
          details: track_data.title,
          state: `by ${track_data.user.username}`,
          startTimestamp,
          endTimestamp,
          largeImageKey: keys[0],
          largeImageText: track_data.title,
          smallImageKey: keys[1],
          smallImageText: track_data.user.username,
          trackURL: request_data.url,
          trackDuration: track_data.duration
        };

        debug("Everything ok, updating activity.", activity_data);
        rpc.setActivity(activity_data)
        .then(() => {
          rpc.setActivityTimeout(endTimestamp + WAIT_BEFORE_CLEAR);

          success();
        })
        .catch(error);
      })
      .catch(error);
    })
    .catch(error);

    }
    catch(err) {
      error(err);
    }
  });
  };
};

================================================
FILE: src/protocols/client.js
================================================
const trace = require('debug')('soundcloud-rp:trace');
const debug = require('debug')('soundcloud-rp:client-protocol');

module.exports = function(config, io, rpc) {

  const activity = require('../procedures/activity')(config, rpc);

  io.on('connection', function(socket){
    trace('client.connection', socket.id);

    socket.on('activity', function(data){
      trace('client.event', socket.id, 'activity', data);

      activity(data)
      .then(() => {
        socket.emit('activity', true, {});
      })
      .catch((err) => {
        socket.emit('activity', false, {
          error: err.name,
          message: err.message
        });
      })
    });

    socket.on('disconnect', function(){
      trace('client.disconnect', socket.id);
    });
  });
};

================================================
FILE: src/routes/client.js
================================================
const router = require('express').Router();
const trace = require('debug')('soundcloud-rp:trace');

router.get('/client.js', (req, res) => {
  trace('GET client');

  res.set('content-type', 'application/javascript');
  res.render('client', {
    host: req.get('host')
  });
});

module.exports = router;

================================================
FILE: src/rpc.js
================================================
const { Client } = require('discord-rpc');
const trace = require('debug')('soundcloud-rp:trace');
const debug = require('debug')('soundcloud-rp:rpc');

const WAIT_BETWEEN_TRIES = 10;
const TIMEOUT = 10;

module.exports = (config) => {

  class RPCWrapper {
    constructor(config, rpc) {
      trace("rpc.constructor");

      this._config = config;
      this._rpc = rpc;

      this.activity_timeout = 0;
      this.current_activity = false;

      this.initRPC();
    }

    initRPC() {
      trace("rpc.init");

      this.status = false;

      this._rpc.on('ready', () => {
        trace("rpc.event.ready");
      });

      this.connect();
    }

    connect() {
      trace("rpc.connect");
      debug("Connecting to discord...");

      this._rpc.login({clientId: this._config.discord.ClientID })
      .then(() => {
        trace("rpc.connect.success");
        debug("Connected to discord!");

        this.status = true;
      })
      .catch((err) => {
        trace("rpc.connect.fail");
        this.status = false;

        debug("Failed to connect to discord", err);
        debug(`Trying again in ${WAIT_BETWEEN_TRIES} seconds...`);
        setTimeout(() => {
          this.connect()
        }, WAIT_BETWEEN_TRIES * 1000);
      });
    }

    setActivity(data) {
      trace("rpc.setActivity", data);

      function pad(str) {
        while (str.length < 2)
          str += " ";
        return str;
      }

      data.details = pad(data.details);
      data.state = pad(data.state);
      data.largeImageText = pad(data.largeImageText);
      data.smallImageText = pad(data.smallImageText);

      this.current_activity = data;

      // We need to timeout ourselves, this method doesn't throw any error
      return new Promise((resolve, reject) => {

        var request_timeout = setTimeout(() => {
          trace("rpc.setActivity.fail");
          this.status = false;
          var err = new Error('RPC timeout.');

          debug("Failed to interact with discord", err);
          debug(`Reconnecting again in ${WAIT_BETWEEN_TRIES} seconds...`);
          setTimeout(() => {
            this.connect()
          }, WAIT_BETWEEN_TRIES * 1000);

          reject(err);
        }, TIMEOUT * 1000);

        this._rpc.setActivity(data).then(() => {
          trace("rpc.setActivity.success");

          clearTimeout(request_timeout);
          resolve();
        });
      });
    }

    getActivity() {
      trace("rpc.getActivity");

      return this.current_activity;
    }

    clearActivity() {
      trace("rpc.clearActivity");

      this.current_activity = false;
      this.clearActivityTimeout();

      // We need to timeout ourselves, this method doesn't throw any error
      return new Promise((resolve, reject) => {

        var request_timeout = setTimeout(() => {
          trace("rpc.clearActivity.fail");
          this.status = false;
          var err = new Error('RPC timeout.');

          debug("Failed to interact with discord", err);
          debug(`Reconnecting again in ${WAIT_BETWEEN_TRIES} seconds...`);
          setTimeout(() => {
            this.connect()
          }, WAIT_BETWEEN_TRIES * 1000);

          reject(err);
        }, TIMEOUT * 1000);

        this._rpc.clearActivity().then(() => {
          trace("rpc.clearActivity.success");

          clearTimeout(request_timeout);
          resolve();
        });
      });
    }

    setActivityTimeout(timestamp) {
      trace("rpc.setActivityTimeout", timestamp);
      let now = Math.round(new Date().getTime() / 1000);

      this.clearActivityTimeout();
      this.activity_timeout = setTimeout(() => {
        trace("rpc.setActivityTimeout.timeout");

        this.clearActivity();
      }, (timestamp - now) * 1000);
    }

    clearActivityTimeout() {
      trace("rpc.clearActivityTimeout");
      clearTimeout(this.activity_timeout);
    }
  }

  const rpc = new Client({transport: 'ipc'});

  return new RPCWrapper(config, rpc);
}

================================================
FILE: src/views/client.hbs
================================================
(function(){
  function load_script(url) {
    return new Promise(function (resolve, reject){
      var head = document.getElementsByTagName('head')[0];
      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.onload = resolve;
      script.onerror = reject;
      script.src = url;
      head.appendChild(script);
    })
  }

  load_script('http://{{host}}/socket.io/socket.io.js')
  .then(function(){
    var socket = io.connect('http://{{host}}');
    var interval = 10;

    function poll_activity() {
      var $title = document.querySelector(".playbackSoundBadge__titleLink"),
        $progress = document.querySelector(".playbackTimeline__progressWrapper"),
        $play = document.querySelector(".playControls__play");

      if (!$title || !$progress || !$play)
        return;

      var url = "https://soundcloud.com" + $title.getAttribute("href"),
        pos = parseInt($progress.getAttribute("aria-valuenow"), 10),
        playing = $play.classList.contains("playing");

      if (!playing)
        return;

      socket.emit('activity', { url, pos });
    }

    poll_activity();
    setInterval(poll_activity, interval * 1000);
  })
  .catch(function(err) {
    console.error('soundcloud-rp', err);
  });

})();
Download .txt
gitextract_wq4eimy9/

├── .gitignore
├── README.md
├── bin/
│   └── soundcloud-rp
├── config/
│   └── default.json
├── package.json
├── soundcloud-rp.sublime-project
├── soundcloud-rp.user.js
└── src/
    ├── helpers/
    │   ├── artwork.js
    │   ├── discord.js
    │   ├── image.js
    │   └── soundcloud.js
    ├── index.js
    ├── procedures/
    │   └── activity.js
    ├── protocols/
    │   └── client.js
    ├── routes/
    │   └── client.js
    ├── rpc.js
    └── views/
        └── client.hbs
Download .txt
SYMBOL INDEX (23 symbols across 6 files)

FILE: soundcloud-rp.user.js
  function load_script (line 12) | function load_script() {

FILE: src/helpers/discord.js
  function getAssetList (line 6) | function getAssetList() {
  function uploadAsset (line 17) | function uploadAsset(type, key, data) {
  function deleteAsset (line 33) | function deleteAsset(id) {

FILE: src/helpers/image.js
  function imageDataFromFile (line 7) | function imageDataFromFile(pathname) {
  function imageDataFromUrl (line 20) | function imageDataFromUrl(url) {

FILE: src/helpers/soundcloud.js
  function getTrackData (line 6) | function getTrackData(url) {
  function sanitizeArtworkUrl (line 18) | function sanitizeArtworkUrl(url) {

FILE: src/procedures/activity.js
  constant WAIT_BEFORE_CLEAR (line 6) | const WAIT_BEFORE_CLEAR = 15;
  function processArtwork (line 13) | function processArtwork(type, id, url) {
  function success (line 110) | function success() {
  function error (line 115) | function error(err) {

FILE: src/rpc.js
  constant WAIT_BETWEEN_TRIES (line 5) | const WAIT_BETWEEN_TRIES = 10;
  constant TIMEOUT (line 6) | const TIMEOUT = 10;
  class RPCWrapper (line 10) | class RPCWrapper {
    method constructor (line 11) | constructor(config, rpc) {
    method initRPC (line 23) | initRPC() {
    method connect (line 35) | connect() {
    method setActivity (line 58) | setActivity(data) {
    method getActivity (line 100) | getActivity() {
    method clearActivity (line 106) | clearActivity() {
    method setActivityTimeout (line 138) | setActivityTimeout(timestamp) {
    method clearActivityTimeout (line 150) | clearActivityTimeout() {
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (27K chars).
[
  {
    "path": ".gitignore",
    "chars": 2448,
    "preview": "\n# Created by https://www.gitignore.io/api/node,linux,windows,sublimetext\n\n### Linux ###\n*~\n\n# temporary files which can"
  },
  {
    "path": "README.md",
    "chars": 3614,
    "preview": "<img src=\"assets/default.png?raw=true\" width=\"128\" height=\"128\" align=\"left\">\n<h1>Soundcloud Rich Presence</h1>\nAdds Dis"
  },
  {
    "path": "bin/soundcloud-rp",
    "chars": 1476,
    "preview": "#!/usr/bin/env node\n\n/**\n * Module dependencies.\n */\n\nvar server = require('../src/');\nvar debug = require('debug')('sou"
  },
  {
    "path": "config/default.json",
    "chars": 176,
    "preview": "{\n  \"discord\": {\n    \"ClientID\": \"443074120758853643\",\n    \"APIKey\": null\n  },\n  \"soundcloud\": {\n    \"ClientID\": \"{inser"
  },
  {
    "path": "package.json",
    "chars": 671,
    "preview": "{\n  \"name\": \"soundcloud-rp\",\n  \"version\": \"2.0.3\",\n  \"description\": \"Adds Discord Rich Presence support to Soundcloud.\","
  },
  {
    "path": "soundcloud-rp.sublime-project",
    "chars": 45,
    "preview": "{\n\t\"folders\":\n\t[\n\t\t{\n\t\t\t\"path\": \".\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "soundcloud-rp.user.js",
    "chars": 894,
    "preview": "// ==UserScript==\n// @name         Soundcloud Rich Presence\n// @namespace    https://github.com/demaisj/soundcloud-rp\n//"
  },
  {
    "path": "src/helpers/artwork.js",
    "chars": 81,
    "preview": "module.exports = {\n  MAX_ARTWORK: 150,\n  ARTWORK_TRACK: 1,\n  ARTWORK_ARTIST: 2\n};"
  },
  {
    "path": "src/helpers/discord.js",
    "chars": 1129,
    "preview": "const request = require('request-promise-native');\nconst trace = require('debug')('soundcloud-rp:trace');\n\nmodule.export"
  },
  {
    "path": "src/helpers/image.js",
    "chars": 964,
    "preview": "const request = require('request-promise-native');\nconst trace = require('debug')('soundcloud-rp:trace');\nconst DataURI "
  },
  {
    "path": "src/helpers/soundcloud.js",
    "chars": 585,
    "preview": "const request = require('request-promise-native');\nconst trace = require('debug')('soundcloud-rp:trace');\n\nmodule.export"
  },
  {
    "path": "src/index.js",
    "chars": 1387,
    "preview": "const express = require('express');\nconst http = require('http');\nconst socketio = require('socket.io')\nconst path = req"
  },
  {
    "path": "src/procedures/activity.js",
    "chars": 5666,
    "preview": "const trace = require('debug')('soundcloud-rp:trace');\nconst debug = require('debug')('soundcloud-rp:activity');\nconst {"
  },
  {
    "path": "src/protocols/client.js",
    "chars": 767,
    "preview": "const trace = require('debug')('soundcloud-rp:trace');\nconst debug = require('debug')('soundcloud-rp:client-protocol');\n"
  },
  {
    "path": "src/routes/client.js",
    "chars": 304,
    "preview": "const router = require('express').Router();\nconst trace = require('debug')('soundcloud-rp:trace');\n\nrouter.get('/client."
  },
  {
    "path": "src/rpc.js",
    "chars": 3960,
    "preview": "const { Client } = require('discord-rpc');\nconst trace = require('debug')('soundcloud-rp:trace');\nconst debug = require("
  },
  {
    "path": "src/views/client.hbs",
    "chars": 1276,
    "preview": "(function(){\n  function load_script(url) {\n    return new Promise(function (resolve, reject){\n      var head = document."
  }
]

About this extraction

This page contains the full source code of the demaisj/soundcloud-rp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (24.8 KB), approximately 6.5k tokens, and a symbol index with 23 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!