Automate YouTube channel list

This was becoming tedious to maintain, so I wrote some code that will
maintain it for me
This commit is contained in:
Devon Campbell 2024-11-22 12:07:09 -05:00
commit fc92223b23
2 changed files with 135 additions and 374 deletions

View file

@ -0,0 +1,120 @@
// Finds your FreeTube profiles.db and adds the channel objects divided by profile to global data
// Access this anywhere via `freetubeCategories`, which will return an array of profile objects
// Note: I refer to 'profiles' as 'categories' since that naming makes more sense given how I'm using them in this automation and on my site. In this documentation, a "category" is the same thing as a FreeTube "Profile."
//
// Category object structure:
// - `name`: string - The name of the category/profile as assigned in FreeTube
// - 'subscriptions': array of subscription objects - The channels added to this category. See below for the subscription object structure. Note that channels added to multiple categories are distinct objects, so making a change to a channel object in one category does not affect that same channel's object if it appears in another category.
// - `bgColor`: string - The background color chosen for the category in FreeTube as a hexidecimal string with leading `#`
// - `textColor`: string - The foreground color chosen for the category in FreeTube as a hexidecimal string with leading `#`
// - `_id`: string - A unique ID generated by FreeTube to track the category. This is generally a seemingly random string of alphanumeric characters, but in the case of the "All Channels" category, the `_id` value is `"allChannels"`.
// - `$$deleted`: boolean - This property only exists on categories that have been deleted. Deleted categories seem to only persist in profiles.db while the app is still running and are removed when the app is closed.
// Note: You may see multiple instances of each category while the app is still running. Make sure to close it before you generate anything from this data.
//
// Subscription object structure:
// - `id`: string - The YouTube channel ID. You can build a URL to this channel by using this format: https://www.youtube.com/channel/{id}
// - `name`: string - The channel's name
// - `thumbnail`: string - A URI pointing to the channel's thumbnail
// - `tags`: string - For any channels assigned to tag categories, this property will contain a string with all of that channel's emoji tags
// - `selected`: boolean - Not entirely sure what this is to be perfectly honest.
//
// This would be more generally useful if I parameterized some things like the tag categories, the config location, and some others, but I won't bother with that for now.
// Maybe in a future revision… or maybe someone would like to submit that as a pull request!
const fs = require('fs');
const fsPromises = fs.promises;
const path = require('path');
const os = require('os');
const homeDir = os.homedir();
const flatpakConfigPath = path.join(homeDir, '.var', 'app', 'io.freetubeapp.FreeTube', 'config', 'FreeTube');
const nonFlatpakConfigPath = path.join(homeDir, '.config', 'FreeTube');
const freetubeConfigLocationByOS = {
linux: () => fs.existsSync(flatpakConfigPath) ? flatpakConfigPath : nonFlatpakConfigPath,
darwin: () => path.join(homeDir, 'Library', 'Application Support', 'FreeTube'),
win32: () => path.join(process.env.APPDATA, 'FreeTube'),
};
module.exports = async function getSubscriptions() {
const osName = os.platform();
if (!(osName in freetubeConfigLocationByOS)) {
console.warn(`Unsupported OS: ${osName}`);
return [];
}
const configLocation = freetubeConfigLocationByOS[osName]();
const subsLocation = path.join(configLocation, 'profiles.db');
// Instead of creating a new channel category, tag categories will add an emoji to a channel's `tag` property.
// Add your own categories to this object.
// The key should be the full name of the category in FreeTube.
// The value should be the emoji (or any unicode character, for that matter) that the channels should be prepended with.
// Channels in one or more tag category will have a `tags` property containing a string with all of that channel's tags.
// If the channel is in multiple categories, each instance of it will receive the `tags` property with the same value.
const tagCategories = {
'Instant Watch': '👀',
Virtuosic: '🤌',
Relaxing: '😌',
};
const categories = [];
const privateSubscriptionIds = new Set();
const subscriptionTags = {};
let allChannels;
try {
await fsPromises.access(subsLocation);
} catch (error) {
console.warn(`Subscriptions file not found at ${subsLocation}`, error);
return [];
}
try {
const data = await fsPromises.readFile(subsLocation, { encoding: 'utf8' });
const lines = data.split('\n');
for (const line of lines) {
if (line.trim()) {
let category;
try {
category = JSON.parse(line);
} catch (error) {
console.error(`Error parsing line: ${line}`, error);
}
if (category.name === 'Private') {
category.subscriptions?.forEach(sub => privateSubscriptionIds.add(sub.id));
} else if (category._id === 'allChannels') {
allChannels = category.subscriptions || [];
} else if (tagCategories.hasOwnProperty(category.name)) {
const tagEmoji = tagCategories[category.name];
category.subscriptions?.forEach(channel => subscriptionTags[channel.id] = (subscriptionTags[channel.id] || '') + tagEmoji);
} else if (!category.$$deleted) {
categories.push(category);
}
}
}
} catch (error) {
console.error(`Error reading file: ${subsLocation}`, error);
return [];
}
const channelsInCategory = new Set(categories.flatMap(category => category.subscriptions.map(channel => channel.id)));
const othersCategory = {
name: "Others",
subscriptions: allChannels.filter(channel =>
!privateSubscriptionIds.has(channel.id) &&
!channelsInCategory.has(channel.id)
)
};
// Categories are sorted alphabetically. Subscriptions are already alphabetized in the incoming data.
const sortedCategories = categories.sort((a, b) => a.name.localeCompare(b.name));
sortedCategories.push(othersCategory);
sortedCategories.forEach(category => {
category.subscriptions?.forEach(channel => channel.tags = subscriptionTags[channel.id])
});
return sortedCategories;
};