<file_sep>const download = require('./lib/download').download;
const slack = require('./lib/slack').slack;
const trim = require('./lib/trim').trim;
const { tweet, upload } = require('./lib/tweet');
const getVideos = require('./lib/video').getVideos;

module.exports = {
  download,
  slack,
  trim,
  tweet,
  upload,
  getVideos
};
<file_sep>require('dotenv').config();
const url = process.env.SLACK_INCOMING_WEBHOOK;
if (!url) {
  console.error('Please set SLACK_INCOMING_WEBHOOK into .env');
}

exports.slack = function slack(text) {
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify({
      username: 'youtube-to-twitter',
      text
    })
  });
};
<file_sep>const { timeline, isDupulicated } = require('./timeline');

require('dotenv').config();

const endpoint = 'https://www.googleapis.com/youtube/v3/playlistItems';
const key = process.env.YOUTUBE_DATA_API_KEY;
const playlist = process.env.YOUTUBE_PLAYLIST_ID;

async function getVideos(playlistId = playlist) {
  const response = await fetch(
    `${endpoint}?part=snippet,contentDetails,status&maxResults=50&playlistId=${playlistId}&key=${key}`,
    {
      headers: {
        Referer: 'https://www.hackforplay.xyz/'
      }
    }
  );
  const json = await response.text();
  const data = JSON.parse(json);

  if (!response.ok) {
    console.error(data.error);
    throw new Error(data.error.message);
  }

  const tl = await timeline();

  // 直近のツイートで紹介していない動画
  const videos = [];
  let notPublishedYet = 0;
  let alreadyTweeted = 0;

  for (const item of data.items) {
    const {
      snippet: { title },
      contentDetails: { videoId, videoPublishedAt }
    } = item;
    if (!videoPublishedAt) {
      notPublishedYet += 1;
      continue; // not published yet
    }
    const url = `https://www.youtube.com/watch?v=${videoId}`;
    if (isDupulicated(tl, url)) {
      alreadyTweeted += 1;
      continue; // already tweeted
    }

    videos.push({
      title,
      url
    });
  }

  if (videos.length === 0) {
    console.log(`fetched ${data.items.length}`);
    console.log(`tweeted ${alreadyTweeted}`);
    console.log(`waiting ${notPublishedYet}`);
    throw new Error('No video found');
  }
  return videos;
}

exports.getVideos = getVideos;
<file_sep># YouTube to Twitter

Download video from youtube channel and upload it twitter.

## Install

> npm install --save youtube-to-twitter

## Example

See [server.js](server.js)

```javascript
const express = require('express');
const { download, trim, tweet, upload, video } = require('youtube-to-twitter');

const app = express();

app.get('/', async (req, res) => {
  res.sendStatus(200);

  main()
    .then(console.log)
    .catch(console.error);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`listen on port ${port}`);
});

async function main() {
  const start = parseInt(process.env.VIDEO_START, 10) || 0;
  const duration = parseInt(process.env.VIDEO_DURATION, 10) || 30;

  const { title, url } = await video();
  const status =
    title.replace(/【ハックフォープレイ実況】/, ' #HackforPlay') +
    '\n\nつづきはこちら↓\n' +
    url;
  console.log('next tweet:\n', status);

  const source = await download(url);
  const output = await trim(source, start, start + duration);

  const mediaId = await upload(output);
  await tweet(mediaId, status);
}
```
<file_sep>const tmp = require('tmp');
const ytdl = require('ytdl-core');
const fs = require('fs');

function download(url) {
  const filepath = tmp.tmpNameSync({ postfix: '.mp4' });

  return new Promise((resolve, reject) => {
    ytdl(url, {
      filter: (format) =>
        format.container === 'mp4' && format.qualityLabel === '720p'
    })
      .pipe(fs.createWriteStream(filepath))
      .on('error', reject)
      .on('close', () => {
        console.log('download complete: ', filepath);
        resolve(filepath);
      });
  });
}

exports.download = download;
<file_sep>const client = require('./tweet').client;

function timeline() {
  return client.get('statuses/user_timeline', {
    count: 200,
    trim_user: true,
    exclude_replies: true
  });
}

exports.timeline = timeline;

function isDupulicated(timeline, url) {
  for (const tweet of timeline) {
    const {
      entities: { urls }
    } = tweet;
    if (!Array.isArray(urls) || urls.length < 1) continue;
    for (const { expanded_url } of urls) {
      if (url === expanded_url) {
        return true;
      }
    }
  }
  return false;
}

exports.isDupulicated = isDupulicated;
<file_sep>const express = require('express');
const fs = require('fs').promises;
const { download, slack, trim, tweet, upload, getVideos } = require('./index');

const app = express();

app.get('/', (req, res) => {
  res.sendStatus(200);
});

app.get('/update', async (req, res) => {
  res.sendStatus(200);
  const dryRun = req.query.dryRun === 'true'; // ?dryRun=true でモード有効化

  main({ dryRun }).then(console.log).catch(console.error);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`listen on port ${port}`);
});

async function main({ dryRun = false }) {
  const start = parseInt(process.env.VIDEO_START, 10) || 0;
  const duration = parseInt(process.env.VIDEO_DURATION, 10) || 30;

  let lastError = null; // ひとつもツイート出来なかった場合 Slack に通知する

  try {
    const videos = await getVideos();
    videos.reverse().splice(10); // 古い順に最大10件までリトライ

    // 直近で紹介していない動画を順番にダウンロードとアップロードを試みる
    for (const { title, url } of videos) {
      let mediaId = '';
      try {
        const source = await download(url);
        const output = await trim(source, start, duration);
        await fs.unlink(source);

        mediaId = await upload(output);
        await fs.unlink(output);
      } catch (error) {
        console.warn(error);
        lastError = error; // エラーを保持して次へ
        continue;
      }

      if (dryRun) {
        return;
      }

      // アップロードに成功したのでツイートして終了
      const status =
        title.replace(/【ハックフォープレイ実況】/, '') +
        '\n\nつづきはこちら↓\n' +
        url;
      console.log('next tweet:\n', status);
      await tweet(mediaId, status);

      return;
    }

    if (lastError) {
      if (dryRun) {
        console.error(lastError);
        return;
      }
      throw lastError;
    }
  } catch (error) {
    try {
      await slack(error.message);
    } catch (error) {
      console.warn('Failed to send slack webhook!', error);
    }
  }
}
