Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# .npmrc
engine-strict=true
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# SplitScreen.Me Hub 📦

<img src="https://www.splitscreen.me/img/splitscreen-me-logo.png" alt="SplitScreen.Me Logo" width="100" height="100"></img>

![CI/CD](https://github.com/SplitScreen-Me/splitscreenme-hub/workflows/CI/badge.svg)
Expand All @@ -14,64 +15,80 @@ Feel free to [contribute](#contribute) and help us build the most amazing **hub
## Basic API 🔥

Handler research:
```

```json
url: "api/v1/handlers/:search_text",
httpMethod: "get"
```

Get all handlers (up to 500):
```

```json
url: "api/v1/allhandlers",
httpMethod: "get"
```

Specific handler infos:
```

```json
url: "api/v1/handler/:handler_id",
httpMethod: "get"
```

Get available packages for one handler:
```

```json
url: "api/v1/packages/:handler_id",
httpMethod: "get"
```

Get package info:
```

```json
url: "api/v1/packages/:package_id",
httpMethod: "get"
```

Get comments done by users about a handler:
```

```json
url: "api/v1/comments/:handler_id",
httpMethod: "get"
```

Download a package from it's ID:
```

```json
url: /cdn/storage/packages/:package_id/original/handler-{handler_id}-v{version_of_handler}.nc?download=true
httpMethod: "get"
```

Get IGDB screenshots for a handler:
```

```json
url: "api/v1/screenshots/:handler_id",
httpMethod: "get"
```

## Contribute

### Prerequisites

1. IDE or text editor. for example [WebStorm](https://www.jetbrains.com/webstorm/) or [VSCode](https://code.visualstudio.com/)
2. IDE for MongoDB, we recommend [NoSQLBooster](https://nosqlbooster.com/)

#### Installation

You must use Node v12 and not a higher version.

```sh
npm install
```
$ npm install
```

#### Local Development

```sh
npm run dev
```
$ npm run dev
```

This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
85 changes: 85 additions & 0 deletions imports/api/Handlers/server/github-methods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Meteor } from 'meteor/meteor';
import axios from 'axios';

// authenticate with GitHub API
Meteor.startup(() => {
Meteor.setInterval(() => {
axios
.get('https://api.github.com/rate_limit')
.then(response => {
if (response.data.resources.core.remaining < 10) {
console.log('GitHub API rate limit reached, refreshing token.');
axios.defaults.headers.common.Authorization = `token ${Meteor.settings.private.GITHUB_API_TOKEN}`;
}
})
.catch(error => {
console.log(error);
});
}, 1000 * 60 * 60 * 24 * 10);
});

/** Get the total download count of all release assets in a GitHub repository */
export const getGitHubDownloads = async (owner, repo) => {
let downloadCount = 0;
let page = 1;
let hasMore = true;

try {
while (hasMore) {
const response = await axios.get(`https://api.github.com/repos/${owner}/${repo}/releases`, {
params: {
page,
per_page: 100, // Max items per page (GitHub's limit)
},
headers: {
'User-Agent': 'Your-App-Name', // GitHub requires this
Accept: 'application/vnd.github.v3+json',
// Uncomment for authenticated requests (recommended):
// Authorization: `token ${process.env.GITHUB_TOKEN}`
},
});

const releases = response.data;

// No more releases (empty array)
if (releases.length === 0) {
hasMore = false;
break;
}

// Sum downloads from all assets
for (const release of releases) {
for (const asset of release.assets) {
downloadCount += asset.download_count;
}
}

// Check for pagination (GitHub uses Link headers)
const linkHeader = response.headers.link;
hasMore = linkHeader?.includes('rel="next"');
page++;
}

return downloadCount;
} catch (error) {
// Handle rate limits (403) and other errors
if (error.response?.status === 403) {
const resetTime = new Date(error.response.headers['x-ratelimit-reset'] * 1000);
console.error(`GitHub API rate limited. Resets at: ${resetTime}`);
} else {
console.error('GitHub API error:', error.message);
}
return 0; // Fallback value
}
};

/** Get the star count of a GitHub repository */
export const getGitHubStars = async (owner, repo) => {
try {
const data = await axios.get(`https://api.github.com/repos/${owner}/${repo}`);
return data.data.stargazers_count;
} catch (e) {
console.log(e);
return 0;
}
};
83 changes: 52 additions & 31 deletions imports/api/Handlers/server/publications.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import Handlers from '../Handlers';
import Comments from '../../Comments/Comments';
import escapeRegExp from '../../../modules/regexescaper';
import Packages from '../../Packages/server/ServerPackages';
import axios from "axios";
import { bearerToken } from "./igdb-methods";
import axios from 'axios';
import { bearerToken } from './igdb-methods';
import { getGitHubStars, getGitHubDownloads } from './github-methods';
import formatNumbers from '../../../modules/formatNumbers';

Meteor.publish(
'handlers',
Expand All @@ -16,7 +18,6 @@ Meteor.publish(
limit = 18,
localHandlerIds = [],
) {

const isSearchFromArray = localHandlerIds.length > 0;

let sortObject = { trendScore: handlerSortOrder === 'up' ? 1 : -1 };
Expand All @@ -36,7 +37,8 @@ Meteor.publish(
if (handlerOptionSearch === 'alphabetical') {
sortObject = { gameName: handlerSortOrder === 'up' ? -1 : 1 };
}
const searchInArraySelectorCondition = isSearchFromArray > 0 ? {_id: { $in: localHandlerIds }} : {};
const searchInArraySelectorCondition =
isSearchFromArray > 0 ? { _id: { $in: localHandlerIds } } : {};

return Handlers.find(
{
Expand Down Expand Up @@ -87,21 +89,20 @@ Meteor.publish(
},
);


/* This code is a PoC, its dirty */
const screenshotsCache = {};

WebApp.connectHandlers.use('/api/v1/screenshots', async (req, res, next) => {
res.writeHead(200);
const handlerId = req.url.split("/")[1];
if(handlerId.length > 0){
const handler = await Handlers.findOne({ _id: handlerId }, {fields: {gameId: 1}});
if(!handler?.gameId) {
res.end(JSON.stringify({error: 'Incorrect handlerId'}));
const handlerId = req.url.split('/')[1];
if (handlerId.length > 0) {
const handler = await Handlers.findOne({ _id: handlerId }, { fields: { gameId: 1 } });
if (!handler?.gameId) {
res.end(JSON.stringify({ error: 'Incorrect handlerId' }));
return;
}

if(!screenshotsCache[handler.gameId]){
if (!screenshotsCache[handler.gameId]) {
const igdbApi = axios.create({
baseURL: 'https://api.igdb.com/v4/',
timeout: 2500,
Expand All @@ -112,49 +113,68 @@ WebApp.connectHandlers.use('/api/v1/screenshots', async (req, res, next) => {
Accept: 'application/json',
},
});
const igdbAnswer = await igdbApi.post('screenshots', `fields *;where game = ${handler.gameId};`);
const igdbAnswer = await igdbApi.post(
'screenshots',
`fields *;where game = ${handler.gameId};`,
);
screenshotsCache[handler.gameId] = igdbAnswer.data;
}
res.end(JSON.stringify({screenshots: screenshotsCache[handler.gameId]}));
}else{
res.end(JSON.stringify({error: 'No handler ID provided'}))
res.end(JSON.stringify({ screenshots: screenshotsCache[handler.gameId] }));
} else {
res.end(JSON.stringify({ error: 'No handler ID provided' }));
}
res.end(JSON.stringify({error: 'Unknown error'}))
})
res.end(JSON.stringify({ error: 'Unknown error' }));
});
/* End of dirty PoC */

WebApp.connectHandlers.use('/api/v1/hubstats', async (req, res, next) => {
res.writeHead(200);
let downloadsSum = 0;
let hotnessSum = 0;
let handlerCount = 0;
let usersCount = 0;
// TODO: Store these in a database and update it every few hours
const totalNucleusCoopGitHubStars =
(await getGitHubStars('SplitScreen-Me', 'splitscreenme-nucleus')) +
(await getGitHubStars('ZeroFox5866', 'nucleuscoop')) +
(await getGitHubStars('nucleuscoop', 'nucleuscoop')) +
(await getGitHubStars('distrohelena', 'nucleuscoop'));
const totalNucleusCoopGitHubDownloads =
(await getGitHubDownloads('SplitScreen-Me', 'splitscreenme-nucleus')) +
(await getGitHubDownloads('ZeroFox5866', 'nucleuscoop')) +
(await getGitHubDownloads('nucleuscoop', 'nucleuscoop')) +
(await getGitHubDownloads('distrohelena', 'nucleuscoop'));

const allPackages = Packages.collection.find({}).fetch();
allPackages.forEach(pkg => {
for (pkg of allPackages) {
if (pkg.meta.downloads > 0) {
downloadsSum = downloadsSum + pkg.meta.downloads;
}
});
}
const allHandlers = Handlers.find({ private: false }).fetch();
allHandlers.forEach(hndl => {
for (const hndl of allHandlers) {
if (hndl.stars > 0) {
hotnessSum = hotnessSum + hndl.stars;
}
});
handlerCount = allHandlers.length;
}

const allUsers = Meteor.users.find({}).fetch();
usersCount = allUsers.length;
const usersCount = allUsers.length;

const usersWithHandlersCount = Meteor.users
.find({ 'profile.handlerId': { $exists: true } })
.fetch().length;

const allComments = Comments.find({}).fetch();
const commentsCount = allComments.length;

res.end(
`Total downloads: ${downloadsSum}` +
`\nTotal hotness: ${hotnessSum}` +
`\nTotal handlers: ${handlerCount}` +
`\nTotal users: ${usersCount}` +
`\nTotal comments ${commentsCount}`
`Total downloads: ${formatNumbers(downloadsSum)}` +
`\nTotal hotness: ${formatNumbers(hotnessSum)}` +
`\nTotal handlers: ${formatNumbers(allHandlers.length)}` +
`\nTotal users: ${formatNumbers(usersCount)}` +
`\nTotal handler authors: ${formatNumbers(usersWithHandlersCount)}` +
`\nTotal comments ${formatNumbers(commentsCount)}` +
`\nTotal Nucleus Co-Op GitHub stars: ${formatNumbers(totalNucleusCoopGitHubStars)}` +
`\nTotal Nucleus Co-Op GitHub downloads: ${formatNumbers(totalNucleusCoopGitHubDownloads)}`,
);
});

Expand Down Expand Up @@ -185,7 +205,8 @@ Meteor.publish(
function handlersFull() {
return Handlers.find(
{
private: false, publicAuthorized: true
private: false,
publicAuthorized: true,
},
{
sort: { stars: -1 },
Expand Down
4 changes: 4 additions & 0 deletions imports/modules/formatNumbers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Format numbers with spaces*/
export default number => {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"dev": "meteor --settings settings-development.json",
"lint": ""
},
"engines": {
"node": "<=13.0.0"
},
"dependencies": {
"@babel/runtime": "^7.14.8",
"@cleverbeagle/seeder": "^1.3.1",
Expand Down
1 change: 1 addition & 0 deletions settings-development.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"private": {
"IGDB_API_KEY": "",
"GITHUB_PERSONAL_ACCESS_TOKEN": "",
"MAIL_URL": "",
"DISCORD_BOT_SECRET_TOKEN": "",
"DISCORD_ADMIN_LOGGING_WEBHOOK": "",
Expand Down