Full Code of jacobwgillespie/plex-sync for AI

master 24e4f5c63edb cached
14 files
19.8 KB
6.2k tokens
8 symbols
1 requests
Download .txt
Repository: jacobwgillespie/plex-sync
Branch: master
Commit: 24e4f5c63edb
Files: 14
Total size: 19.8 KB

Directory structure:
gitextract_q_7pllf5/

├── .babelrc
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── bin/
│   └── plex-sync
├── package.json
├── resources/
│   └── travis_after_all.py
└── src/
    ├── env.js
    ├── index.js
    ├── plex.js
    ├── rateLimit.js
    └── ui.js

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

================================================
FILE: .babelrc
================================================
{
  "presets": ["es2015", "stage-0"],
  "plugins": ["transform-runtime"]
}


================================================
FILE: .eslintrc
================================================
{
  "parser": "babel-eslint",
  "extends": "airbnb-base",
  "rules": {
    "no-restricted-syntax": [
      "error",
      "ForInStatement",
      "LabeledStatement",
      "WithStatement"
    ]
  }
}


================================================
FILE: .gitignore
================================================
node_modules/
lib/
.env
*.log


================================================
FILE: .travis.yml
================================================
language: node_js

node_js:
- node
- '4'

env:
  global:
    - CXX=g++-4.8

addons:
  apt:
    sources:
      - ubuntu-toolchain-r-test
    packages:
      - g++-4.8

cache:
  directories:
  - node_modules
  - $HOME/.yarn-cache

before_install:
- npm config set spin false --global

script:
- npm run lint && npm run test

after_failure:
- "(cd resources; python travis_after_all.py)"

after_success:
- "(cd resources; python travis_after_all.py)"
- export $(cat resources/.to_export_back)

deploy:
  provider: npm
  email: jacobwgillespie@gmail.com
  api_key:
    secure: HdjDzyh/M6SN4wM8macctTUTr438wOGq6S0mkNEgwBQ0aPM1AqQ3uuI8C6AkS+0YiDS9DD8429n+c55UrPMFlmx5JNe56FTOrwTvWHzG53GB66Tm10Zv+mGVciKcQK5CEBUVLffznpyTS07sb7RFJN8BBAIACp5G18G0+Gd3JxKvsYlXD7/hDIqR72tqSYkzlX1rNXJLKG3X39t05ss9V10JYcfR3joXgIryIUS1/qbiKCckZ+61odgpwEm222HEdfOERbRz4I8VP+WuT29idfpEjVv5D++7GaYlUOjno3QiPi1QxRQkJEnBnAl9/6l+LHm4po01Wmaa8MTMsSJT9HW3Atw06VHcY5qzaGk2zZ1BXLGjmKHSGRj7QgzK5vY00DWM0/N+GJJM8COKCtNGoWVv2ogclRSuYumDl1JL+3gwkz5kqkUMOY9n1JDZJqXMkWfAe10QSWpYK7LG1+6yVLpfjcQB2q+DtiU91pB7gz5b7JTUZHNkr7WYkCqkPotoQ9XMQXSgBBFnX/rg2FcEZO4WKsMUuxzeRylhMgm9NVvDKZx7wHhZy+GrneYBbjNkSDC+jqK4ZSU/MLufU0ISZ3l7GjFRiWL0c99QScYEHlo7DINRmrDTV1n4mih15iBJqp9Z+DnF+WbHUZ29usc+BYHhvOZcwyZGHVQAXnceRZY=
  on:
    tags: true
    repo: jacobwgillespie/plex-sync
    branch: master
    condition: "$BUILD_LEADER$BUILD_AGGREGATE_STATUS = YESothers_succeeded"


================================================
FILE: LICENSE
================================================
Copyright (c) 2016 Jacob Gillespie <jacobwgillespie@gmail.com>

Permission  is  hereby granted, free of charge, to any person ob-
taining a copy of  this  software  and  associated  documentation
files  (the "Software"), to deal in the Software without restric-
tion, including without limitation the rights to use, copy, modi-
fy, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is  fur-
nished to do so, subject to the following conditions:

The  above  copyright  notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF  ANY  KIND,
EXPRESS  OR  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE  AND  NONIN-
FRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER  IN  AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN  THE
SOFTWARE.


================================================
FILE: README.md
================================================
# plex-sync

A simple command-line utility to synchronize watched / seen status between different [Plex Media Servers](https://plex.tv).

[![npm](https://img.shields.io/npm/v/plex-sync.svg?maxAge=2592000)](https://www.npmjs.com/package/plex-sync)
[![Travis](https://img.shields.io/travis/jacobwgillespie/plex-sync.svg?maxAge=2592000)](https://travis-ci.org/jacobwgillespie/plex-sync)
[![Dependencies](https://david-dm.org/jacobwgillespie/plex-sync.svg)](https://david-dm.org/jacobwgillespie/plex-sync)
[![Greenkeeper badge](https://badges.greenkeeper.io/jacobwgillespie/plex-sync.svg)](https://greenkeeper.io/)
![MIT license](https://img.shields.io/badge/license-MIT-blue.svg?maxAge=2592000)

[![asciicast](https://asciinema.org/a/9j3oyj46vugcc039l7tbxecw4.png)](https://asciinema.org/a/9j3oyj46vugcc039l7tbxecw4)

## Features

* Syncs watch status between different Plex servers.

## Requirements

* NodeJS 4+

## Installation

`plex-sync` is installed via NPM:

```shell
$ npm install -g plex-sync
```

## Usage

There are several available configuration environment variables:

Variable | Description
-------- | -----------
`PLEX_TOKEN` | The API token used to access your Plex server.  To locate this token, follow the [instructions here](https://support.plex.tv/hc/en-us/articles/204059436-Finding-your-account-token-X-Plex-Token), or use [this bookmarklet](https://jacobwgillespie.github.io/plex-token-bookmarklet/).  **The Plex token must be set on the arguments or via the environment variable.**
`DRY_RUN` | Set this environment variable to make `plex-sync` print out what it was planning to do rather than actually perform the synchronization.
`MATCH_TYPE` | Can be either `fuzzy` (default) or `precise`.  When the matching is fuzzy, the script will match items by their year and title.  When the matching is precise, the script matches items by their internal Plex GUID, which is usually the IMDb or TMDb ID.  This requires an individual API request to be performed for each item (each movie, each TV episode, etc.) and thus is very slow and can potentially overwhelm and crash the Plex server.  Use at your own risk.
`RATE_LIMIT` | Default `5`.  If the `MATCH_TYPE` is set to `precise`, this is the maximum number of concurrent API requests `plex-sync` will make to your server to fetch GUIDs.  Use this to (potentially) alleviate performance issues with precise matching.

First, find the IDs for the libraries on each server you would like to sync.  These IDs can be found at the end of the URL when viewing the library in your browser (like `.../section/ID`).

Next, use the CLI as follows:

```shell
$ plex-sync [https://][token@]IP[:PORT]/SECTION[,rw] [https://][token@]IP[:PORT]/SECTION[,rw]
            [[https://][token@]IP[:PORT]/SECTION[,rw]...]
```

### Examples

Sync watched status between two servers, using the default port (`32400`), using library ID `1` for the first server and library `3` for the second:

```shell
$ plex-sync 10.0.1.5/1 10.0.1.10/3
```

Sync three servers, with different ports:

```shell
$ plex-sync 10.0.1.5:32401/1 10.0.1.5:32402/1 10.0.1.10/3
```

Sync with a server via HTTPS:

```shell
$ plex-sync 10.0.1.2/2 https://server-domain/3
```

Dry run, to see what the script will do:

```shell
$ DRY_RUN=1 plex-sync 10.0.1.5/1 10.0.1.5/1
```

Precise matching (slow and may crash the Plex server):

```shell
$ MATCH_TYPE=precise plex-sync 10.0.1.5/1 10.0.1.5/1
```

Syncing between multiple Plex users (different access tokens):

```shell
$ plex-sync xxxxxx@10.0.1.5/1 zzzzzz@10.0.1.10/3
```

Unidirectional sync (read from one server, write to the other):

```shell
$ plex-sync 10.0.1.5/1,r 10.0.1.10/3,w
```

Complex use case:

```shell
$ plex-sync xxxx@10.0.1.5:32401/1,r https://yyyy@10.0.1.10/3,w zzzz@10.0.1.15/2,rw
```

For more complex strategies, like syncing between multiple different library mappings, just run the tool multiple times.  If you need to run the synchronization on a schedule, use another scheduling tool like cron.  These more advanced features may be added in the future, but currently `plex-sync` is very simple.

## Contributing

Contributions are welcome.  Open a pull request or issue to contribute.

## License

MIT license.  See `LICENSE` for more information.


================================================
FILE: bin/plex-sync
================================================
#!/usr/bin/env node
require('../lib/index');


================================================
FILE: package.json
================================================
{
  "name": "plex-sync",
  "version": "0.6.1",
  "description": "Sync watched status between Plex servers",
  "main": "lib/index.js",
  "bin": {
    "plex-sync": "bin/plex-sync"
  },
  "files": [
    "bin",
    "lib",
    "src",
    "LICENSE",
    "README.md"
  ],
  "scripts": {
    "build": "babel src --out-dir lib/",
    "lint": "eslint --ignore-path .gitignore .",
    "preversion": "npm test",
    "prepublish": "npm run build",
    "sync": "node lib/index.js",
    "test": "npm run build",
    "watch": "npm run build -- --watch"
  },
  "author": "Jacob Gillespie <jacobwgillespie@gmail.com>",
  "license": "MIT",
  "dependencies": {
    "babel-core": "^6.24.1",
    "babel-runtime": "^6.23.0",
    "dotenv": "^4.0.0",
    "isomorphic-fetch": "^2.2.1",
    "ts-progress": "^0.1.2",
    "update-notifier": "^2.1.0",
    "xml2js": "^0.4.17"
  },
  "devDependencies": {
    "babel-cli": "^6.24.1",
    "babel-eslint": "^7.2.2",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "eslint": "^3.19.0",
    "eslint-config-airbnb-base": "^11.1.3",
    "eslint-plugin-import": "^2.2.0"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/jacobwgillespie/plex-sync.git"
  },
  "bugs": {
    "url": "https://github.com/jacobwgillespie/plex-sync/issues"
  },
  "homepage": "https://github.com/jacobwgillespie/plex-sync#readme"
}


================================================
FILE: resources/travis_after_all.py
================================================
"""
https://github.com/dmakhno/travis_after_all/blob/master/travis_after_all.py
"""

import os
import json
import time
import logging

try:
    import urllib.request as urllib2
except ImportError:
    import urllib2

log = logging.getLogger("travis.leader")
log.addHandler(logging.StreamHandler())
log.setLevel(logging.INFO)

TRAVIS_JOB_NUMBER = 'TRAVIS_JOB_NUMBER'
TRAVIS_BUILD_ID = 'TRAVIS_BUILD_ID'
POLLING_INTERVAL = 'LEADER_POLLING_INTERVAL'

build_id = os.getenv(TRAVIS_BUILD_ID)
polling_interval = int(os.getenv(POLLING_INTERVAL, '5'))

#assume, first job is the leader
is_leader = lambda job_number: job_number.endswith('.1')

if not os.getenv(TRAVIS_JOB_NUMBER):
    # seems even for builds with only one job, this won't get here
    log.fatal("Don't use defining leader for build without matrix")
    exit(1)
elif is_leader(os.getenv(TRAVIS_JOB_NUMBER)):
    log.info("This is a leader")
else:
    #since python is subprocess, env variables are exported back via file
    with open(".to_export_back", "w") as export_var:
        export_var.write("BUILD_MINION=YES")
    log.info("This is a minion")
    exit(0)


class MatrixElement(object):
    def __init__(self, json_raw):
        self.is_finished = json_raw['finished_at'] is not None
        self.is_succeeded = json_raw['result'] == 0
        self.number = json_raw['number']
        self.is_leader = is_leader(self.number)


def matrix_snapshot():
    """
    :return: Matrix List
    """
    response = urllib2.build_opener().open("https://api.travis-ci.org/builds/{0}".format(build_id)).read()
    raw_json = json.loads(response)
    matrix_without_leader = [MatrixElement(element) for element in raw_json["matrix"]]
    return matrix_without_leader


def wait_others_to_finish():
    def others_finished():
        """
        Dumps others to finish
        Leader cannot finish, it is working now
        :return: tuple(True or False, List of not finished jobs)
        """
        snapshot = matrix_snapshot()
        finished = [el.is_finished for el in snapshot if not el.is_leader]
        return reduce(lambda a, b: a and b, finished), [el.number for el in snapshot if
                                                        not el.is_leader and not el.is_finished]

    while True:
        finished, waiting_list = others_finished()
        if finished: break
        log.info("Leader waits for minions {0}...".format(waiting_list))  # just in case do not get "silence timeout"
        time.sleep(polling_interval)


try:
    wait_others_to_finish()

    final_snapshot = matrix_snapshot()
    log.info("Final Results: {0}".format([(e.number, e.is_succeeded) for e in final_snapshot]))

    BUILD_AGGREGATE_STATUS = 'BUILD_AGGREGATE_STATUS'
    others_snapshot = [el for el in final_snapshot if not el.is_leader]
    if reduce(lambda a, b: a and b, [e.is_succeeded for e in others_snapshot]):
        os.environ[BUILD_AGGREGATE_STATUS] = "others_succeeded"
    elif reduce(lambda a, b: a and b, [not e.is_succeeded for e in others_snapshot]):
        log.error("Others Failed")
        os.environ[BUILD_AGGREGATE_STATUS] = "others_failed"
    else:
        log.warn("Others Unknown")
        os.environ[BUILD_AGGREGATE_STATUS] = "unknown"
    #since python is subprocess, env variables are exported back via file
    with open(".to_export_back", "w") as export_var:
        export_var.write("BUILD_LEADER=YES {0}={1}".format(BUILD_AGGREGATE_STATUS, os.environ[BUILD_AGGREGATE_STATUS]))

except Exception as e:
    log.fatal(e)


================================================
FILE: src/env.js
================================================
require('dotenv').config();


================================================
FILE: src/index.js
================================================
import updateNotifier from 'update-notifier';

import { exitUsage, log, parseCLIArg, progressMap } from './ui';
import { fetchMovies, markWatched } from './plex';
import pkg from '../package.json';

import './env';

updateNotifier({ pkg }).notify();

const DRY_RUN = !!process.env.DRY_RUN;
const FUZZY = (process.env.MATCH_TYPE || 'fuzzy') === 'fuzzy';

if (process.argv.length < 4) {
  exitUsage();
}

const servers = process.argv.slice(2).map(parseCLIArg);

(async () => {
  try {
    log(`Reading data from ${servers.map(server => server.host).join(', ')}...`);

    const movies = await progressMap(
      servers,
      server => fetchMovies(server, FUZZY),
    );

    const watched = new Set();

    for (const [idx, serverMovies] of movies.entries()) {
      const server = servers[idx];

      if (server.mode.read) {
        serverMovies.forEach((movie) => {
          if (movie.watched) watched.add(movie.guid);
        });
      }
    }

    log('Syncing any unsynced media...');

    for (const [idx, serverMovies] of movies.entries()) {
      const server = servers[idx];

      if (server.mode.write) {
        const needsSync = serverMovies.filter(
          movie => !movie.watched && watched.has(movie.guid),
        );

        // Note: the await here is intentional - we want to process servers one at a time
        await progressMap( // eslint-disable-line no-await-in-loop
          needsSync,
          (media) => {
            if (DRY_RUN) {
              log(`Dry run: marking ${media.title} watched on ${server.host}`);
              return;
            }

            markWatched(server, media);
          },
          DRY_RUN,
        );
      }
    }

    log('Sync completed!');
  } catch (e) {
    log(e.stack);
  }
})();


================================================
FILE: src/plex.js
================================================
import { parseString } from 'xml2js';
import fetch from 'isomorphic-fetch';

import { concurrent } from './rateLimit';
import { progressMap } from './ui';

import './env';

const PAGE_SIZE = 32;

const parseXML = xml => new Promise((resolve, reject) => {
  parseString(xml, (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});

const fetchText = url =>
  fetch(url)
  .then(res => res.text());

const fetchXML = url =>
  fetchText(url)
  .then(res => parseXML(res));

const rateLimitFetchXML = url =>
  concurrent(fetchXML, url);

const flatten = list => list.reduce(
  (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [],
);

const fetchMediaContainer = async (server, page = 1) => {
  const start = (page - 1) * PAGE_SIZE;
  const url = `${server.protocol}://${server.host}/library/sections/${server.section}/allLeaves?X-Plex-Token=${server.token}&X-Plex-Container-Start=${start}&X-Plex-Container-Size=${PAGE_SIZE}`;
  return rateLimitFetchXML(url);
};

export const fetchMedia = async (server) => {
  // Determine total collection size
  const totalSize = parseInt((
    await fetchMediaContainer(server)
  ).MediaContainer.$.totalSize, 10);
  const totalPages = Math.ceil(totalSize / PAGE_SIZE);

  // Fetch all videos
  const promises = [];
  for (let i = 1; i <= totalPages; i += 1) {
    promises.push(fetchMediaContainer(server, i));
  }

  // Unpack video results
  const videos = [];
  await Promise.all(promises).then(
    (results) => {
      results.forEach(
        (res) => {
          res.MediaContainer.Video.forEach(
            (video) => {
              videos.push(video);
            },
          );
        },
      );
    },
  );

  // Map videos into entries
  return videos
  .map(
    media => media.$,
  )
  .map(({
    grandparentTitle = '',
    index = '',
    key,
    parentIndex = '',
    title,
    viewCount,
    year,
  }) => ({
    guid: `${grandparentTitle} - ${parentIndex} - ${index} - ${year} - ${title}`,
    key,
    title,
    watched: parseInt(viewCount || '0', 10) > 0,
    year,
  }));
};

export const fetchMediaGUID = async (server, media) => {
  const url = `${server.protocol}://${server.host}${media.key}?X-Plex-Token=${server.token}`;
  return rateLimitFetchXML(url)
  .then(res => res.MediaContainer.Video[0].$)
  .then(({ guid }) => ({
    ...media,
    guid,
  }));
};

export const fetchMovies = async (server, fuzzy = true) => {
  const media = await fetchMedia(server);
  return fuzzy ? media : progressMap(
    media,
    movie => fetchMediaGUID(server, movie),
  );
};

const extractID = key => key.match(/\/library\/metadata\/(\d+)/)[1];

export const markWatched = async (server, movie) => fetchText(
  `${server.protocol}://${server.host}/:/scrobble?identifier=com.plexapp.plugins.library&key=${extractID(movie.key)}&X-Plex-Token=${server.token}`,
);

export const markUnatched = async (server, movie) => fetchText(
  `${server.protocol}://${server.host}/:/unscrobble?identifier=com.plexapp.plugins.library&key=${extractID(movie.key)}&X-Plex-Token=${server.token}`,
);


================================================
FILE: src/rateLimit.js
================================================
const current = [];
const backlog = [];
export const limit = parseInt(process.env.RATE_LIMIT || '5', 10);
export const concurrent = (fn, ...args) => {
  const enqueue = ([promise, resolve, fn2, ...args2]) => {
    current.push(promise);
    resolve(fn2(...args2));
    promise.then(
      (res) => {
        current.splice(current.indexOf(promise), 1);

        if (current.length < limit && backlog.length > 0) {
          enqueue(backlog.pop());
        }
        return res;
      },
    );
    return promise;
  };

  let resolve;
  const promise = new Promise((res) => {
    resolve = res;
  });

  if (current.length < limit) {
    enqueue([promise, resolve, fn, ...args]);
  } else {
    backlog.push([promise, resolve, fn, ...args]);
  }

  return promise;
};


================================================
FILE: src/ui.js
================================================
/* eslint-disable no-console */

import Progress from 'ts-progress';

export const log = (...args) => console.error(...args);

export const TOKEN = process.env.PLEX_TOKEN;

export const exitUsage = () => {
  console.error(`
Usage: plex-sync [https://][token@]IP[:PORT]/SECTION[,rw] [https://][token@]IP[:PORT]/SECTION[,rw]
                 [[https://][token@]IP[:PORT]/SECTION[,rw]...]

Example:

    Sync section 1 on a server with the default port with section 2 on another server:
    $ plex-sync 10.0.1.2/1 10.0.1.3:32401/2

    Sync three servers:
    $ plex-sync 10.0.1.2/1 10.0.1.3/1 10.0.1.4/1

    Sync with a server via HTTPS
    $ plex-sync 10.0.1.2/2 https://server-domain/3

    Dry run, to see what the script will do:
    $ DRY_RUN=1 plex-sync 10.0.1.5/1 10.0.1.5/1

    Precise matching (slow and may crash the Plex server):
    $ MATCH_TYPE=precise plex-sync 10.0.1.5/1 10.0.1.5/1

    Syncing between multiple Plex users (different access tokens):
    $ plex-sync xxxxxx@10.0.1.5/1 zzzzzz@10.0.1.10/3

    Unidirectional sync (read from one server, write to the other):
    $ plex-sync 10.0.1.5/1,r 10.0.1.10/3,w

    Complex use case:
    $ plex-sync xxxx@10.0.1.5:32401/1,r https://yyyy@10.0.1.10/3,w zzzz@10.0.1.15/2,rw
`.trim());
  process.exit(1);
};

export const exitToken = () => {
  console.error(`
Error: missing Plex authentication token for one or more of the specified servers

Please either set your Plex authentication token via the PLEX_TOKEN environment variable
or pass the Plex token in the server definition (TOKEN@0.0.0.0...).  See 'plex-sync help'
for more information.

If you need help locating your Plex authentication token, feel free to use the bookmarklet
located at https://jacobwgillespie.github.io/plex-token-bookmarklet/
`.trim());
  process.exit(1);
};

export const progressMap = (items, fn, disable = false) => {
  const progress = disable ? null : Progress.create({ total: items.length });

  return Promise.all(items.map((...args) => {
    if (!disable) progress.update();
    return fn(...args);
  }));
};

export const parseCLIArg = (arg) => {
  const matches = arg.match(/^((https?):\/\/)?(([^@]+)@)?(([^:]+)(:\d+)?)\/(\d+)(,[rw][rw]?)?$/);
  if (!matches) exitUsage();
  const protocol = matches[2] === 'https' ? 'https' : 'http';
  const token = matches[4] || TOKEN;
  const host = `${matches[6]}${matches[7] || ':32400'}`;
  const section = matches[8] || '1';
  const modeString = matches[10] || 'rw';
  const mode = {
    read: modeString.includes('r'),
    write: modeString.includes('w'),
  };

  if (!token) exitToken();

  return { protocol, token, host, section, mode };
};
Download .txt
gitextract_q_7pllf5/

├── .babelrc
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── bin/
│   └── plex-sync
├── package.json
├── resources/
│   └── travis_after_all.py
└── src/
    ├── env.js
    ├── index.js
    ├── plex.js
    ├── rateLimit.js
    └── ui.js
Download .txt
SYMBOL INDEX (8 symbols across 4 files)

FILE: resources/travis_after_all.py
  class MatrixElement (line 43) | class MatrixElement(object):
    method __init__ (line 44) | def __init__(self, json_raw):
  function matrix_snapshot (line 51) | def matrix_snapshot():
  function wait_others_to_finish (line 61) | def wait_others_to_finish():

FILE: src/index.js
  constant DRY_RUN (line 11) | const DRY_RUN = !!process.env.DRY_RUN;
  constant FUZZY (line 12) | const FUZZY = (process.env.MATCH_TYPE || 'fuzzy') === 'fuzzy';

FILE: src/plex.js
  constant PAGE_SIZE (line 9) | const PAGE_SIZE = 32;

FILE: src/ui.js
  constant TOKEN (line 7) | const TOKEN = process.env.PLEX_TOKEN;
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (22K chars).
[
  {
    "path": ".babelrc",
    "chars": 75,
    "preview": "{\n  \"presets\": [\"es2015\", \"stage-0\"],\n  \"plugins\": [\"transform-runtime\"]\n}\n"
  },
  {
    "path": ".eslintrc",
    "chars": 200,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": \"airbnb-base\",\n  \"rules\": {\n    \"no-restricted-syntax\": [\n      \"error\",\n    "
  },
  {
    "path": ".gitignore",
    "chars": 30,
    "preview": "node_modules/\nlib/\n.env\n*.log\n"
  },
  {
    "path": ".travis.yml",
    "chars": 1410,
    "preview": "language: node_js\n\nnode_js:\n- node\n- '4'\n\nenv:\n  global:\n    - CXX=g++-4.8\n\naddons:\n  apt:\n    sources:\n      - ubuntu-t"
  },
  {
    "path": "LICENSE",
    "chars": 1118,
    "preview": "Copyright (c) 2016 Jacob Gillespie <jacobwgillespie@gmail.com>\n\nPermission  is  hereby granted, free of charge, to any p"
  },
  {
    "path": "README.md",
    "chars": 4245,
    "preview": "# plex-sync\n\nA simple command-line utility to synchronize watched / seen status between different [Plex Media Servers](h"
  },
  {
    "path": "bin/plex-sync",
    "chars": 45,
    "preview": "#!/usr/bin/env node\nrequire('../lib/index');\n"
  },
  {
    "path": "package.json",
    "chars": 1425,
    "preview": "{\n  \"name\": \"plex-sync\",\n  \"version\": \"0.6.1\",\n  \"description\": \"Sync watched status between Plex servers\",\n  \"main\": \"l"
  },
  {
    "path": "resources/travis_after_all.py",
    "chars": 3504,
    "preview": "\"\"\"\nhttps://github.com/dmakhno/travis_after_all/blob/master/travis_after_all.py\n\"\"\"\n\nimport os\nimport json\nimport time\ni"
  },
  {
    "path": "src/env.js",
    "chars": 28,
    "preview": "require('dotenv').config();\n"
  },
  {
    "path": "src/index.js",
    "chars": 1754,
    "preview": "import updateNotifier from 'update-notifier';\n\nimport { exitUsage, log, parseCLIArg, progressMap } from './ui';\nimport {"
  },
  {
    "path": "src/plex.js",
    "chars": 3063,
    "preview": "import { parseString } from 'xml2js';\nimport fetch from 'isomorphic-fetch';\n\nimport { concurrent } from './rateLimit';\ni"
  },
  {
    "path": "src/rateLimit.js",
    "chars": 768,
    "preview": "const current = [];\nconst backlog = [];\nexport const limit = parseInt(process.env.RATE_LIMIT || '5', 10);\nexport const c"
  },
  {
    "path": "src/ui.js",
    "chars": 2641,
    "preview": "/* eslint-disable no-console */\n\nimport Progress from 'ts-progress';\n\nexport const log = (...args) => console.error(...a"
  }
]

About this extraction

This page contains the full source code of the jacobwgillespie/plex-sync GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (19.8 KB), approximately 6.2k tokens, and a symbol index with 8 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!