diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff19033 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +package-lock.json +.env \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..edc41c5 --- /dev/null +++ b/css/style.css @@ -0,0 +1,265 @@ +body { + background-color: #f1f1f1; + font-family: Arial, Helvetica, sans-serif +} + +* { + box-sizing: border-box +} + +h1, +p { + text-align: center +} + +.card { + background-color: #fff; + padding: 1vw; + margin-top: 1vw; + border: 1px solid #ccc; + width: 48vw +} + +.banner { + width: 100%; + height: 15vh; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + margin-top: 0; + margin-bottom: 0 +} + +@media screen and (max-width:600px) { + .banner { + height: 7vh + } +} + +.name { + margin-left: 0; + font-size: 2vw; + margin-top: 0; + font-weight: bolder; + margin-bottom: 0 +} + +h4 { + font-size: 1.5vw; + margin-top: 0; + margin-bottom: 0 +} + +h5 { + font-size: 1vw; + margin-top: 0; + margin-bottom: 0 +} + +.logo { + height: 100%; + object-fit: cover; + border-radius: 50%; + border: solid #000 .3vw +} + +.stats { + margin-top: 0; + margin-bottom: 0; + display: grid; + grid-template-columns: 1fr 1fr +} + +hr { + margin-top: 0; + margin-bottom: 0 +} + +.description { + margin-top: 0; + margin-bottom: 0; + width: 100%; + font-size: 1vw; + overflow: hidden; + resize: vertical +} + +.add { + font-size: 2vw; + margin-bottom: .5vw +} + +.popup { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, .5); + display: none; + justify-content: center; + align-items: center +} + +.loaderBack { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, .3); + display: none; + justify-content: center; + align-items: center +} + +@keyframes loading { + 0% { + transform: rotate(0) + } + + 100% { + transform: rotate(360deg) + } +} + +.loading { + border: 1vw solid #f3f3f3; + border-radius: 50%; + border-top: 1vw solid #3498db; + animation: loading 2s linear infinite; + width: 10vw; + height: 10vw; + position: absolute; + top: 50%; + left: 50%; + margin-top: -5vw; + margin-left: -5vw +} + +.popup2 { + position: fixed; + width: 50%; + height: 75%; + top: 20%; + left: 25%; + background-color: #fff; + justify-content: center; + align-items: center; + text-align: center; + border-radius: 3%; + border: solid 4px #000; + overflow-x: scroll +} + +#name { + width: 95%; + height: 10%; + font-size: 2vw; + margin-top: 0; + margin-bottom: 0 +} + +.search2 { + width: 95%; + height: 10%; + font-size: 2vw; + margin-top: 0; + margin-bottom: 0 +} + +.popup-header { + display: flex; + justify-content: space-between; + width: 90%; + margin-top: 0 +} + +.close { + font-size: 2vw; + margin-top: 0; + margin-bottom: 0; + margin-left: 0; + background-color: transparent; + border: none; + cursor: pointer +} + +.addBulk { + font-size: 2vw; + margin-top: 0; + margin-bottom: 0; + margin-left: 0; + background-color: transparent; + border: none; + cursor: pointer +} + +.addChannel { + margin-left: 0; + font-size: 3vw +} + +.search_handle, +.search_name, +.search_stats { + margin-top: 0; + margin-bottom: 0 +} + +.search_avatar { + height: 7vw; + width: 7vw +} + +.search_name { + font-size: 1.5vw +} + +.search_handle { + font-size: 1vw +} + +.search_stats { + font-size: 1vw; + margin-bottom: .5vw +} + +.search_button { + font-size: 2vw +} + +.title { + font-size: 3vw; + margin-top: 0; + margin-bottom: 0 +} + +#bulk { + width: 95%; + height: 10%; + font-size: 1vw; + margin-top: 0; + margin-bottom: 0 +} + +.spanList { + background-color: #000; + color: #fff +} + +#popup-content2 { + display: none +} + +.channels { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 1vw +} + +button, +option, +select { + color: #000 +} \ No newline at end of file diff --git a/favicons/android-chrome-192x192.png b/favicons/android-chrome-192x192.png new file mode 100644 index 0000000..30a7bbe Binary files /dev/null and b/favicons/android-chrome-192x192.png differ diff --git a/favicons/android-chrome-512x512.png b/favicons/android-chrome-512x512.png new file mode 100644 index 0000000..f2461e1 Binary files /dev/null and b/favicons/android-chrome-512x512.png differ diff --git a/favicons/apple-touch-icon.png b/favicons/apple-touch-icon.png new file mode 100644 index 0000000..73cd1cd Binary files /dev/null and b/favicons/apple-touch-icon.png differ diff --git a/favicons/browserconfig.xml b/favicons/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/favicons/favicon-16x16.png b/favicons/favicon-16x16.png new file mode 100644 index 0000000..2bfdc3a Binary files /dev/null and b/favicons/favicon-16x16.png differ diff --git a/favicons/favicon-32x32.png b/favicons/favicon-32x32.png new file mode 100644 index 0000000..82e1d67 Binary files /dev/null and b/favicons/favicon-32x32.png differ diff --git a/favicons/favicon.ico b/favicons/favicon.ico new file mode 100644 index 0000000..3532882 Binary files /dev/null and b/favicons/favicon.ico differ diff --git a/favicons/mstile-150x150.png b/favicons/mstile-150x150.png new file mode 100644 index 0000000..e2fb439 Binary files /dev/null and b/favicons/mstile-150x150.png differ diff --git a/favicons/safari-pinned-tab.svg b/favicons/safari-pinned-tab.svg new file mode 100644 index 0000000..bdbe6b6 --- /dev/null +++ b/favicons/safari-pinned-tab.svg @@ -0,0 +1,573 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/favicons/site.webmanifest b/favicons/site.webmanifest new file mode 100644 index 0000000..b20abb7 --- /dev/null +++ b/favicons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/functions/addUser.js b/functions/addUser.js new file mode 100644 index 0000000..928ae29 --- /dev/null +++ b/functions/addUser.js @@ -0,0 +1,102 @@ +import db from './db.js'; +import axios from 'axios'; +import getKey from './getKey.js'; +import searchChannel from './searchChannel.js'; +import updateUser from './updateUser.js'; + +const addUser = async (userId) => { + if (((!userId.startsWith('@')) && ((!userId.startsWith('UC')) || (!userId.length == 24)))) { + let r = await searchChannel(userId); + return r; + } else if (await db.find('id', userId)) { + updateUser(userId); + return { + error: true, + message: 'User already exists' + } + } else { + if (userId.startsWith('@')) { + userId = await axios.get('https://yt.lemnoslife.com/channels?handle=' + userId) + .then(async (response) => { + if (response.data.items) { + return response.data.items[0].id + } + return { + error: true, + message: 'Error while adding user, this error was not your fault!' + } + }) + .catch((error) => { + console.log(error); + return { + error: true, + message: 'Error while adding user, this error was not your fault!' + } + }); + } + if (await db.find('id', userId)) { + updateUser(userId); + return { + error: true, + message: 'User already exists' + } + } + return axios.get(`https://www.googleapis.com/youtube/v3/channels?part=statistics,snippet,brandingSettings&id=${userId}&key=${getKey()}`) + .then(async (response) => { + if (response.data.error) { + console.log(response.data.error); + return { + error: true, + message: 'Error while adding user, this error was not your fault!' + } + } + if (!response.data.items) { + return { + error: true, + message: 'No channel found with that ID' + } + } else { + db.add({ + id: response.data.items[0].id, + created: Date.now(), + updated: Date.now(), + deleted: { + deleted: false, + date: null + }, + user: { + name: response.data.items[0].snippet.title, + logo: response.data.items[0].snippet.thumbnails.default.url, + banner: response.data.items[0].brandingSettings.image ? response.data.items[0].brandingSettings.image.bannerExternalUrl : response.data.items[0].snippet.thumbnails.default.url, + country: response.data.items[0].brandingSettings.channel.country, + joined: response.data.items[0].snippet.publishedAt, + description: response.data.items[0].snippet.description, + deleted: { + deleted: false, + date: null + } + }, + stats: { + views: parseInt(response.data.items[0].statistics.viewCount), + subscribers: parseInt(response.data.items[0].statistics.subscriberCount), + videos: parseInt(response.data.items[0].statistics.videoCount) + } + }); + return { + error: false, + message: 'User added successfully' + } + } + + }) + .catch((error) => { + console.error(error); + return { + error: true, + message: 'Error while updating user, this error was not your fault!' + } + }); + } +}; + +export default addUser; \ No newline at end of file diff --git a/functions/db.js b/functions/db.js new file mode 100644 index 0000000..70a0620 --- /dev/null +++ b/functions/db.js @@ -0,0 +1,92 @@ +import mongoose from 'mongoose'; +import dotenv from 'dotenv'; +dotenv.config(); + +await mongoose.connect('mongodb://'+process.env.MONGO_URL, { + useNewUrlParser: true, + authSource: 'admin', + user: process.env.MONGO_USER, + pass: process.env.MONGO_PASSWORD +}) + .then(() => console.log('Connected!')).catch(err => console.log(err)); + +let userSchema = new mongoose.Schema({ + id: String, + created: Number, + updated: Number, + deleted: Object, + user: Object, + stats: Object +}); + +let User = mongoose.model('users', userSchema); + +const add = async (json) => { + try { + let newUser = new User(json); + await newUser.save(); + } catch (error) {} +}; + +const update = async (id, value) => { + try { + await User.updateOne({ id: id }, { $set: value }); + } catch (error) {} +} + +const find = async (type, value) => { + try { + const document = await User.findOne({ [type]: value }); + if (document) return document; + return null; + } catch (error) {} +}; + +const getall = async () => { + try { + const documents = await User.find({}, { id: 1, _id: 1 }); + return documents; + } catch (error) {} +}; + +const getall2 = async (options) => { + try { + let sort1 = options.sort1 === "views" || options.sort1 === "subscribers" || options.sort1 === "videos" ? `stats.${options.sort1}` : `user.${options.sort1}`; + let sort2 = options.sort2 === "views" || options.sort2 === "subscribers" || options.sort2 === "videos" ? `stats.${options.sort2}` : `user.${options.sort2}`; + let documents = await User.find({ + $or: [ + { "user.name": { $regex: options.search, $options: "i" } }, + { "user.id": { $regex: options.search, $options: "i" } }, + ], + }) + .sort({ + [sort1]: options.order1 === "asc" ? 1 : -1, + [sort2]: options.order2 === "asc" ? 1 : -1, + }).limit(options.limit).skip(options.offset); + return documents; + } catch (error) {} +}; + +const find2 = async (json) => { + try { + const documents = await User.find(json); + return documents; + } catch (error) {} +}; + +const getTotalDocuments = async () => { + try { + const count = await User.countDocuments(); + return count; + } catch (error) {} +}; + +export default { + add, + find, + find2, + getall, + update, + getall2, + getTotalDocuments +}; \ No newline at end of file diff --git a/functions/getKey.js b/functions/getKey.js new file mode 100644 index 0000000..8590007 --- /dev/null +++ b/functions/getKey.js @@ -0,0 +1,8 @@ +import dotenv from 'dotenv'; +dotenv.config(); +function getKey() { + let keys = process.env.YOUTUBE_API_KEYS.split(','); + let key = keys[Math.floor(Math.random() * keys.length)]; + return key; +} +export default getKey; \ No newline at end of file diff --git a/functions/searchChannel.js b/functions/searchChannel.js new file mode 100644 index 0000000..2b9ea66 --- /dev/null +++ b/functions/searchChannel.js @@ -0,0 +1,30 @@ +import youtubesearchapi from 'youtube-search-api' +import db from './db.js'; + +const searchChannel = async (search) => { + return youtubesearchapi.GetListByKeyword(search, true, 15, [{ type: "channel" }]) + .then(async (response) => { + let ids = response.items.map((item) => item.id); + let channels = await db.find2({ id: { $in: ids } }) + channels = channels.map((channel) => channel.id); + response.items.forEach((item) => { + item.added = channels.includes(item.id); + }); + return { + error: false, + message: 'Channel(s) found', + channels: response.items + }; + }) + .catch((error) => { + console.error(error); + return { + error: true, + message: 'No channel found', + channels: [], + errorObj: error + } + }) +}; + +export default searchChannel; \ No newline at end of file diff --git a/functions/sendChannels.js b/functions/sendChannels.js new file mode 100644 index 0000000..e14c369 --- /dev/null +++ b/functions/sendChannels.js @@ -0,0 +1,6 @@ +import db from './db.js'; +const sendChannels = async (options) => { + return db.getall2(options); +}; + +export default sendChannels; \ No newline at end of file diff --git a/functions/updateUser.js b/functions/updateUser.js new file mode 100644 index 0000000..fb39595 --- /dev/null +++ b/functions/updateUser.js @@ -0,0 +1,63 @@ +import db from './db.js'; +import axios from 'axios'; +import getKey from './getKey.js'; + +const updateUser = async (userId) => { + let user = await db.find('id', userId) + if (user) { + return await axios.get(`https://www.googleapis.com/youtube/v3/channels?part=statistics,snippet,brandingSettings&id=${userId}&key=${getKey()}`) + .then(async (response) => { + if (!response.data.error) { + if (response.data.items) { + if (response.data.items.length === 0) { + return { + error: true, + message: 'Unable to find user' + } + } + user = { + id: response.data.items[0].id, + created: user.created, + updated: Date.now(), + deleted: user.deleted, + user: { + name: response.data.items[0].snippet.title, + logo: response.data.items[0].snippet.thumbnails.default.url, + banner: response.data.items[0].brandingSettings.image ? response.data.items[0].brandingSettings.image.bannerExternalUrl : response.data.items[0].snippet.thumbnails.default.url, + country: response.data.items[0].brandingSettings.channel.country, + joined: response.data.items[0].snippet.publishedAt, + description: response.data.items[0].snippet.description + }, + stats: { + views: parseInt(response.data.items[0].statistics.viewCount), + subscribers: parseInt(response.data.items[0].statistics.subscriberCount), + videos: parseInt(response.data.items[0].statistics.videoCount) + } + } + db.update(userId, user); + return { + error: false, + message: 'User updated successfully' + } + } + } + return { + error: true, + message: 'Error while updating user, this error was not your fault!' + } + }) + .catch((error) => { + console.log(error); + return { + error: true, + message: 'Error while updating user, this error was not your fault!' + } + }); + } + return { + error: true, + message: 'Unable to find user' + } +}; + +export default updateUser; \ No newline at end of file diff --git a/images/avatar.png b/images/avatar.png new file mode 100644 index 0000000..53aa651 Binary files /dev/null and b/images/avatar.png differ diff --git a/images/banner.png b/images/banner.png new file mode 100644 index 0000000..d495709 Binary files /dev/null and b/images/banner.png differ diff --git a/images/preview.PNG b/images/preview.PNG new file mode 100644 index 0000000..373487e Binary files /dev/null and b/images/preview.PNG differ diff --git a/index.js b/index.js new file mode 100644 index 0000000..a2061db --- /dev/null +++ b/index.js @@ -0,0 +1,120 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import cors from 'cors'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import addUser from './functions/addUser.js'; +import searchChannel from './functions/searchChannel.js'; +import updateUser from './functions/updateUser.js'; +import sendChannels from './functions/sendChannels.js'; +import { fork } from 'child_process'; +import db from './functions/db.js'; + +const app = express(); +app.use(cors()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); +app.set('view engine', 'ejs'); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +app.get('/', async (req, res) => { + res.render('index', { users: await sendChannels({ sort1: 'subscribers', sort2: 'name', order1: 'desc', order2: 'desc', limit: 5, offset: 0, search: '' }), total: await db.getTotalDocuments() }); +}); +app.get('/favicons/*', (req, res) => { + res.sendFile(__dirname + req.path); +}); +app.get('/js/*', (req, res) => { + res.sendFile(__dirname + req.path); +}); +app.get('/css/*', (req, res) => { + res.sendFile(__dirname + req.path); +}); +app.get('/images/*', (req, res) => { + res.sendFile(__dirname + req.path); +}); +app.post('/api/add', async (req, res) => { + if (!req.body.id) { + res.send({ + error: true, + message: 'No Parameters Provided' + }); + } else { + if ((!req.body.id.startsWith('@')) && ((req.body.id.startsWith('UC')) || (req.body.id.length == 24))) { + res.send(await addUser(req.body.id)); + } else if (req.body.id.startsWith('@')) { + res.send(await addUser(req.body.id)); + } else { + res.send(await searchChannel2(req.body.id)); + } + } +}); + +app.post('/api/update/:id', async (req, res) => { + if ((req.body.id.startsWith('UC')) && (req.body.id.length == 24)) { + res.send(await updateUser(req.body.id)); + } else { + res.send({ + error: true, + message: 'Invalid Channel ID' + }); + } +}); + +app.post('/api/channels', async (req, res) => { + let sort1 = req.body.sort1 ? req.body.sort1 : 'subscribers'; + let sort2 = req.body.sort2 ? req.body.sort2 : 'name'; + if (sort1 == "") { + sort1 = 'subscribers'; + } + if (sort2 == "") { + sort2 = 'name'; + } + let options = { + sort1: sort1, + sort2: sort2, + order1: req.body.order1 ? req.body.order1 : 'desc', + order2: req.body.order2 ? req.body.order2 : 'desc', + limit: 5, + offset: req.body.offset ? req.body.offset : 0, + search: req.body.search ? req.body.search : '' + } + res.send(await sendChannels(options)); +}) + +app.post('/api/addBulk', async (req, res) => { + let ids = req.body.ids; + for (let i = 0; i < ids.length; i++) { + if (!ids[i].startsWith('UC')) { + ids.splice(i, 1); + i -= 1; + } else if (ids[i].length != 24) { + ids.splice(i, 1); + i -= 1; + } else { + addUser(ids[i]); + } + } + res.send({ + error: false, + message: 'Adding channels' + }); +}) + +async function searchChannel2(term) { + return await searchChannel(term); +} + +let lastHour = new Date().getHours(); +setInterval(() => { + if (new Date().getHours() != lastHour) { + lastHour = new Date().getHours(); + if (lastHour === 0 || lastHour === 12) { + fork('./functions/updateAll.js'); + } + } +}, 60000); + +app.listen(3002, () => { + console.log('Server running on port 3002'); +}) \ No newline at end of file diff --git a/js/fix.js b/js/fix.js new file mode 100644 index 0000000..664ed0d --- /dev/null +++ b/js/fix.js @@ -0,0 +1,21 @@ +function fix(input) { + input = input.replace('Abonnenten', 'Subscribers'); + if (input.includes('Mio.')) { + input = input.replace(' Mio.', 'M'); + input = input.replace(',', '.'); + if (input.includes('000')) { + input = input.replace('.000', 'K'); + } else if (input.includes('00')) { + input = input.replace('00', 'K'); + } + } else { + if (input.includes(',')) { + input = input.replace(',', '.'); + input = input.replace(' Subscribers', 'K Subscribers'); + } + } + if (input == '0') { + input = '0 Subscribers'; + } + return input; +} \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..91f0440 --- /dev/null +++ b/js/main.js @@ -0,0 +1,235 @@ +let offset = 0; +let end = false; +let searching = false; +let sort1 = 'subscribers'; +let sort2 = 'subscribers'; +let order1 = 'desc'; +let order2 = 'desc'; +async function getChannels() { + fetch('/api/channels', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ offset: offset, sort1: sort1, sort2: sort2, search: document.getElementById('search').value, order1: order1, order2: order2 }) + }) + .then(res => res.json()) + .then(data => { + document.getElementById('loader').style.display = "none"; + if (!data.error) { + if (data.length == 0) { + end = true; + } else { + searching = false; + data.forEach(channel => { + let card = document.createElement('div'); + card.classList.add('card'); + card.innerHTML = ` + +
+

${channel.user.name}


+
+
+

${channel.stats.subscribers.toLocaleString()}

+
Subscribers
+
+
+

${channel.stats.views.toLocaleString()}

+
Views
+
+
+

${channel.stats.videos.toLocaleString()}

+
Videos
+
+
+

${channel.user.country}

+
Country
+
+
+

${new Date(channel.user.joined).toString().split('GMT')[0]}

+
Joined
+
+
+

+ `; + document.querySelector('.channels').appendChild(card); + }); + } + } + }).catch(err => console.error(err)); +} + +function search() { + offset = 0; + end = false; + document.querySelector('.channels').innerHTML = ''; + sort1 = document.getElementById('sort1').value; + sort2 = document.getElementById('sort2').value; + order1 = document.getElementById('order1').value; + order2 = document.getElementById('order2').value; + document.getElementById('loader').style.display = "block"; + searching = true; + getChannels(); +} + +function addChannel() { + document.querySelector('.popup').style.display = 'flex'; +} + +function closePopup() { + document.querySelector('.popup').style.display = 'none'; +} + +let searchPage = 1; +let searchResults = []; +function searchChannel(handle) { + fetch('/api/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id: (handle ? handle : document.getElementById('name').value) }) + }) + .then(res => res.json()) + .then(data => { + if (data.error) { + alert(data.message); + } else if (data.channels) { + searchResults = data.channels; + searchPage = 1; + document.getElementById('results').innerHTML = ''; + for (let q = 0; q < (data.channels.length > 5 ? 5 : data.channels.length); q++) { + loadSearch(data.channels[q]); + } + let pages = `
+ +

${searchPage}/${Math.ceil(data.channels.length / 5)}

+
`; + document.getElementById('results').innerHTML += pages; + } else { + alert(data.message); + } + }) + .catch(err => console.error(err)); +} + +async function loadSearch(user) { + let card = document.createElement('div'); + card.classList.add('card'); + let unadded = `` + let http = user.image.startsWith('https') ? '' : 'https:'; + card.innerHTML = ` + avatar +
+

${user.title}

+

${user.handle}

+

${fix(user.subscribers)}

+ ${user.added ? '' : unadded} +
` + document.getElementById('results').appendChild(card); +} + +function nextPage() { + if (searchPage < Math.ceil(searchResults.length / 5)) { + searchPage++; + document.getElementById('results').innerHTML = ''; + for (let q = (searchPage - 1) * 5; q < (searchPage * 5 > searchResults.length ? searchResults.length : searchPage * 5); q++) { + loadSearch(searchResults[q]); + } + let pages = `
+ +

${searchPage}/${Math.ceil(searchResults.length / 5)}

+
`; + document.getElementById('results').innerHTML += pages; + document.getElementById('results').scrollIntoView(); + } +} + +function prevPage() { + if (searchPage > 1) { + searchPage--; + document.getElementById('results').innerHTML = ''; + for (let q = (searchPage - 1) * 5; q < (searchPage * 5 > searchResults.length ? searchResults.length : searchPage * 5); q++) { + loadSearch(searchResults[q]); + } + let pages = `
+ +

${searchPage}/${Math.ceil(searchResults.length / 5)}

+
`; + document.getElementById('results').innerHTML += pages; + document.getElementById('results').scrollIntoView(); + } +} + +function imgError(type, img) { + if (type === 'avatar') { + img.src = '/images/avatar.png'; + } else { + img.src = '/images/banner.png'; + } +} + +function isAtBottom() { + if (end || searching) return false; + return (window.innerHeight + window.scrollY) >= document.body.offsetHeight; +} + +window.addEventListener('scroll', () => { + if (isAtBottom()) { + offset += 5; + getChannels(); + } +}); + +function sorter() { + document.querySelector('.channels').innerHTML = ''; + offset = 0; + end = false; + searching = true; + sort1 = document.getElementById('sort1').value; + sort2 = document.getElementById('sort2').value; + getChannels(); +} + +function changeOrder() { + document.querySelector('.channels').innerHTML = ''; + offset = 0; + end = false; + searching = true; + order1 = document.getElementById('order1').value; + order2 = document.getElementById('order2').value; + getChannels(); +} + +function addBulk() { + document.getElementById('popup-content').style.display = 'none'; + document.getElementById('popup-content2').style.display = 'block'; +} + +function searchBulk() { + if (document.getElementById('bulk').value.length > 0) { + document.getElementById('popup-content2').style.display = 'none'; + document.getElementById('popup-content').style.display = 'block'; + fetch('/api/addBulk', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ ids: document.getElementById('bulk').value.split(',') }) + }).then(res => res.json()) + .then(data => { + if (data.error) { + alert(data.message); + } else { + document.getElementById('bulk').value = ''; + alert('Adding channels... this may take a while so you may close this tab.'); + } + }).catch(err => console.error(err)); + } else { + alert('Please enter a list of channel IDs!') + document.getElementById('popup-content2').style.display = 'none'; + document.getElementById('popup-content').style.display = 'block'; + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..519a38e --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "the-youtube-list", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^1.4.0", + "body-parser": "^1.20.2", + "child_process": "^1.0.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "express": "^4.18.2", + "mongoose": "^7.3.2", + "youtube-search-api": "^1.2.0" + }, + "type": "module" +} diff --git a/updateAll.js b/updateAll.js new file mode 100644 index 0000000..911f6df --- /dev/null +++ b/updateAll.js @@ -0,0 +1,20 @@ +import db from "./functions/db.js"; +import updateUser from "./functions/updateUser.js"; +let channels = await db.getall() +let index = 0 +updateUserManager() +async function updateUserManager() { + try { + if (index < channels.length) { + await updateUser(channels[index].id) + index++ + updateUserManager() + } else { + console.log("Done") + process.exit() + } + } catch (e) { + index++ + updateUserManager() + } +} \ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..c43934e --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,152 @@ + + + + + + The YouTube List + + + + + + + + + + + + + + + + + + + + + + + + + +
+

The YouTube List

+

There are currently <%= total.toLocaleString() %> channels in the list!

+

Scroll down the list to reveal more channels!

+ +
+
+
+ + + + + + +
+
+
+ <% users.forEach((channel)=> { %> +
+ +
+

+ <%=channel.user.name%> +

+
+
+
+

+ <%=channel.stats.subscribers.toLocaleString()%> +

+
Subscribers
+
+
+

+ <%=channel.stats.views.toLocaleString()%> +

+
Views
+
+
+

+ <%=channel.stats.videos.toLocaleString()%> +

+
Videos
+
+
+

+ <%=channel.user.country ? channel.user.country : 'N/A' %> +

+
Country
+
+
+

+ <%=new Date(channel.user.joined).toString().split('GMT')[0]%> +

+
Joined
+
+
+
+
+ +
+ <% }) %> +
+
+ +
+
+
+ + + + + \ No newline at end of file