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 @@
+
+
+
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} logo](${channel.user.logo})
+
+
+
${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 = `
+
+
+
${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%> logo](<%=channel.user.logo%>)
+
+
+
+ <%=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