Merge pull request #58 from misenhower/develop

Release
This commit is contained in:
Matt Isenhower 2024-11-16 13:32:15 -08:00 committed by GitHub
commit 6029efd782
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 23464 additions and 10182 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
storage

View File

@ -12,15 +12,16 @@ VUE_APP_GOOGLE_ANALYTICS_ID=
# (Optional) Sentry error reporting (https://sentry.io)
SENTRY_DSN=
# (Optional) S3 parameters
AWS_S3_ENDPOINT=
AWS_REGION=
AWS_S3_BUCKET=
AWS_S3_PRIVATE_BUCKET=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# (Optional) Twitter API parameters
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN_KEY=
TWITTER_ACCESS_TOKEN_SECRET=
# (Optional) Deployment server info (user@host)
SHIPIT_PRODUCTION_SERVER=user@host
SHIPIT_PRODUCTION_DIR=/absolute/path
SHIPIT_STAGING_SERVER=user@host
SHIPIT_STAGING_DIR=/absolute/path

46
.eslintrc.cjs Normal file
View File

@ -0,0 +1,46 @@
/* eslint-env node */
// const createAliasSetting = require('@vue/eslint-config-airbnb/createAliasSetting');
module.exports = {
'root': true,
'extends': [
'plugin:vue/vue3-recommended',
'eslint:recommended',
],
'rules': {
// ESLint
// 'indent': ['warn', 2, { 'SwitchCase': 1 }],
'comma-dangle': ['warn', 'always-multiline'],
'no-unused-vars': ['warn', { 'args': 'none' }],
'semi': 'warn',
'quotes': ['warn' , 'single'],
'object-curly-spacing': ['warn', 'always'],
// Vue
'vue/multi-word-component-names': 'off',
'vue/require-default-prop': 'off',
'vue/max-attributes-per-line': ['warn', { singleline: { max: 4 } }],
'vue/html-self-closing': ['warn', { html: { void: 'always' } }],
'vue/no-deprecated-filter': 'off',
'vue/require-toggle-inside-transition': 'off',
'vue/no-deprecated-destroyed-lifecycle': 'off',
},
'globals': {
'__dirname': 'readonly',
'process': 'readonly',
'require': 'readonly',
'module': 'readonly',
'Buffer': 'readonly',
},
'env': {
'vue/setup-compiler-macros': true,
},
'ignorePatterns': [
'src/assets/i18n/index.mjs', // "assert" syntax is currently unrecognized
],
settings: {
// ...createAliasSetting({
// '@': './src',
// }),
},
};

87
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,87 @@
name: Deploy
on:
push:
branches:
- main
- develop
jobs:
deploy-frontend:
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy
uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read
env:
AWS_S3_ENDPOINT: ${{ secrets.AWS_S3_ENDPOINT }}
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
SOURCE_DIR: 'dist'
deploy-backend:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/app/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@ -0,0 +1,14 @@
# Example Docker Compose override file for local development
services:
app:
platform: linux/amd64 # Needed for Apple Silicon
build:
dockerfile: docker/app/Dockerfile
volumes:
- .:/app
browserless:
platform: linux/arm64 # Needed for Apple Silicon
ports:
- 3000:3000

View File

@ -1,20 +0,0 @@
version: '2'
# This adds config options for use with nginx-proxy:
# https://github.com/jwilder/nginx-proxy
# Alternatively, a public port for nginx could be exposed here.
services:
nginx:
environment:
VIRTUAL_HOST: splatoon2.ink,www.splatoon2.ink
networks:
- default
- nginx-proxy
networks:
nginx-proxy:
external:
name: nginxproxy_default

View File

@ -0,0 +1,17 @@
# Example Docker Compose override file for production
services:
app:
# Use a different tag if needed
# image: ghcr.io/misenhower/splatoon2.ink:develop
labels: [ "com.centurylinklabs.watchtower.scope=splatoon2ink" ]
browserless:
labels: [ "com.centurylinklabs.watchtower.scope=splatoon2ink" ]
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 30 --scope splatoon2ink
labels: [ "com.centurylinklabs.watchtower.scope=splatoon2ink" ]

View File

@ -1,17 +1,19 @@
version: '2'
services:
app:
build: docker/app
image: ghcr.io/misenhower/splatoon2.ink
init: true
restart: unless-stopped
volumes:
- ./:/app
environment:
USE_BROWSERLESS: true
BROWSERLESS_ENDPOINT: ws://browserless:3000
SCREENSHOT_HOST: app
depends_on:
- browserless
env_file:
- .env
nginx:
image: nginx
browserless:
image: ghcr.io/browserless/chromium
restart: unless-stopped
volumes:
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
- ./logs:/logs
- ./dist:/usr/share/nginx/html:ro
environment:
CONCURRENT: ${BROWSERLESS_CONCURRENT:-1}

View File

@ -1,22 +1,15 @@
FROM node:12
# Chrome/Puppeteer support
RUN apt-get update \
&& apt-get install -y wget gnupg \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Install dumb-init
# https://github.com/Yelp/dumb-init
# This fixes issues with zombie Chrome processes:
# https://github.com/GoogleChrome/puppeteer/issues/615
RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64.deb &&\
dpkg -i dumb-init_*.deb
FROM node:20
# App setup
WORKDIR /app
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["yarn", "cron"]
ENV PUPPETEER_SKIP_DOWNLOAD=true
# Install NPM dependencies
COPY package*.json ./
RUN npm ci
# Copy app files and build
COPY . .
RUN npm run build
CMD ["npm", "run", "start"]

View File

@ -1,60 +0,0 @@
# Rate limiting zone (Note: 10m is the size of the address pool, not time)
limit_req_zone $binary_remote_addr zone=main:10m rate=10r/s;
server {
listen 80 default_server;
root /usr/share/nginx/html;
index index.html;
charset utf-8;
charset_types application/json;
# Docker reverse proxy IP passthrough
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Real-IP;
# Log to the main (Docker stdout) log
access_log /var/log/nginx/access.log;
# Rate limiting: allow bursts with no delay
limit_req zone=main burst=500 nodelay;
location / {
try_files $uri $uri/ /index.html;
}
# Force browsers to check for updated data
location /data/ {
add_header Cache-Control no-cache;
add_header Access-Control-Allow-Origin *;
autoindex on;
}
location /twitter-images/ {
add_header Cache-Control no-cache;
add_header Access-Control-Allow-Origin *;
}
location ~ \.ics$ {
add_header Content-Type text/calendar;
}
# CORS (for modern ES module support and third-party integrations)
location /assets/ {
add_header Access-Control-Allow-Origin *;
}
# Block access to the file used for screenshots
location = /screenshots.html {
internal;
}
}
# Redirect www to non-www
server {
listen 80;
server_name www.splatoon2.ink;
return 301 $scheme://splatoon2.ink$request_uri;
}

23033
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,15 +6,20 @@
"build": "vue-cli-service build --modern --no-clean",
"lint": "vue-cli-service lint",
"cron": "node src/app/cron",
"start": "npm run sync:download && npm run splatnet && npm run twitter && npm run cron",
"locale-man": "node node_modules/locale-man/ -l en,es,es-MX,fr,fr-CA,de,nl,it,ru,ja -o src/locale",
"splatnet": "node src/app splatnet",
"twitter": "node src/app twitter",
"twitter:test": "node src/app twitterTest",
"sync": "node src/app sync",
"sync:upload": "node src/app syncUpload",
"sync:download": "node src/app syncDownload",
"utility:copyTranslation": "node src/utility copyTranslation",
"utility:getSplatNetLanguageFiles": "node src/utility getSplatNetLanguageFiles",
"utility:updateGear": "node src/utility updateGear"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.685.0",
"axios": "^0.19.2",
"babel-eslint": "^10.1.0",
"bulma": "~0.6.1",
@ -25,18 +30,20 @@
"delay": "^4.4.0",
"dotenv": "^8.2.0",
"ecstatic": "^4.1.4",
"eslint": "^7.6.0",
"eslint-plugin-vue": "^6.2.2",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^9.30.0",
"he": "^1.1.1",
"json-stable-stringify": "^1.0.1",
"jsonpath": "^1.0.0",
"lodash": "^4.17.4",
"make-runnable": "^1.3.6",
"mime-types": "^2.1.35",
"mkdirp": "^1.0.4",
"module-alias": "^2.1.0",
"moment-timezone": "^0.5.13",
"puppeteer": "^13.6.0",
"puppeteer": "^23.7.1",
"raven": "^2.1.1",
"s3-sync-client": "^4.3.1",
"twitter-api-v2": "^1.15.0",
"v-click-outside": "^3.0.1",
"vue": "^2.6.11",
@ -54,25 +61,8 @@
"locale-man": "^0.0.5",
"sass": "^1.50.1",
"sass-loader": "^10",
"shipit-cli": "^5.3.0",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {
"no-prototype-builtins": 0
},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}

View File

@ -1,5 +1,8 @@
# [Splatoon2.ink](https://splatoon2.ink)
> [!NOTE]
> Check out [Splatoon3.ink](https://github.com/misenhower/splatoon3.ink) as well!
Splatoon2.ink shows the current and upcoming map schedules for Splatoon 2.
This site was built with [Vue.js](https://vuejs.org/) and [Bulma](http://bulma.io/).
@ -7,8 +10,6 @@ All data comes from the SplatNet 2 API.
More information about Nintendo's API can be found [here](https://github.com/ZekeSnider/NintendoSwitchRESTAPI).
**Note:** The Splatnet updater script requires Node.js v8 or later.
## Getting Started
Clone this repo:
@ -26,38 +27,27 @@ cp .env.example .env
You can retrieve your `iksm_session` ID value (`NINTENDO_SESSION_ID` inside `.env`) using [Fiddler](http://www.telerik.com/fiddler) or a similar tool.
See [here](https://github.com/frozenpandaman/splatnet2statink#setup-instructions) for further instructions.
I recommend using [Yarn](https://yarnpkg.com/en/) to manage JS dependencies.
```shell
yarn install # Install dependencies
yarn splatnet # Retrieve updates from Splatnet and the Salmon Run calendar
yarn serve # Start the webpack dev server
npm install # Install dependencies
npm run splatnet # Retrieve updates from Splatnet and the Salmon Run calendar
npm run serve # Start the webpack dev server
```
Data retrieved from Splatnet is stored in the `public/data` directory.
Data retrieved from Splatnet is stored in the `dist/data` directory.
By default, the dev server will run on port 8080.
When running `yarn serve` you can access the site by going to http://localhost:8080.
When running `npm run serve` you can access the site by going to http://localhost:8080.
## Production
Build minified assets for production:
```shell
yarn build
npm run build
```
Retrieve updates from Splatnet every hour via [node-cron](https://github.com/kelektiv/node-cron):
```shell
yarn cron
```
## Docker
I use a Docker container on my production server to build production assets and retrieve data from Splatnet.
```shell
sudo docker-compose run --rm app yarn build # Build production assets
sudo docker-compose up -d app # Start periodic updates
npm run cron
```

View File

@ -1,59 +0,0 @@
require('dotenv').config();
module.exports = shipit => {
shipit.initConfig({
production: {
servers: process.env.SHIPIT_PRODUCTION_SERVER,
dir: process.env.SHIPIT_PRODUCTION_DIR,
},
staging: {
servers: process.env.SHIPIT_STAGING_SERVER,
dir: process.env.SHIPIT_STAGING_DIR,
},
});
const dockerApp = 'sudo docker-compose exec -T app ';
function options() {
return { cwd: shipit.config.dir };
}
// Git
shipit.blTask('pull', () => {
return shipit.remote('git pull', options());
});
// JS
shipit.blTask('install-js-deps', () => {
return shipit.remote(dockerApp + 'yarn install', options());
});
shipit.blTask('build-js', ['install-js-deps'], () => {
return shipit.remote(dockerApp + 'yarn run build', options());
});
shipit.blTask('deploy-js', [
'pull',
'build-js',
]);
// App
shipit.blTask('restart-app', () => {
return shipit.remote('sudo docker-compose restart app', options());
});
shipit.blTask('splatnet', () => {
return shipit.remote(dockerApp + 'yarn splatnet', options());
});
// Combination Tasks
shipit.blTask('deploy', [
'pull',
'deploy-js',
'restart-app',
]);
}

View File

@ -4,6 +4,9 @@ module.exports = {
splatnet: require('./updater').updateAll,
twitter: require('./twitter').maybePostTweets,
twitterTest: require('./twitter').testScreenshots,
sync: require('./sync').sync,
syncUpload: require('./sync').syncUpload,
syncDownload: require('./sync').syncDownload,
};
require('make-runnable/custom')({ printOutputFrame: false });

View File

@ -21,18 +21,30 @@ function startHttpServer() {
});
}
function getBrowser() {
// Use Browserless when configured
if (process.env.USE_BROWSERLESS) {
return puppeteer.connect({
browserWSEndpoint: process.env.BROWSERLESS_ENDPOINT,
});
}
// Otherwise just launch normally
return puppeteer.launch({
args: [
'--no-sandbox', // Allow running as root inside the Docker container
],
// headless: false, // For testing
});
}
async function captureScreenshot(options) {
// Create an HTTP server
const server = await startHttpServer();
const { port } = server.address();
// Launch a new Chrome instance
const browser = await puppeteer.launch({
args: [
'--no-sandbox', // Allow running as root inside the Docker container
],
// headless: false, // For testing
});
const browser = await getBrowser();
// Create a new page and set the viewport
const page = await browser.newPage();
@ -40,7 +52,8 @@ async function captureScreenshot(options) {
page.setViewport(thisViewport);
// Navigate to the URL
let url = new URL(`http://localhost:${port}/screenshots.html`);
let host = process.env.SCREENSHOT_HOST || 'localhost';
let url = new URL(`http://${host}:${port}/screenshots.html`);
url.hash = options.hash;
await page.goto(url, {
waitUntil: 'networkidle0', // Wait until the network is idle

79
src/app/sync/S3Syncer.js Normal file
View File

@ -0,0 +1,79 @@
const path = require('path');
const { S3Client } = require('@aws-sdk/client-s3');
const { S3SyncClient } = require('s3-sync-client');
const mime = require('mime-types');
class S3Syncer
{
download() {
this.log('Downloading files...');
return Promise.all([
this.syncClient.sync(this.publicBucket, `${this.localPath}/dist`, {
filters: this.filters,
}),
this.syncClient.sync(this.privateBucket, `${this.localPath}/storage`),
]);
}
upload() {
this.log('Uploading files...');
return Promise.all([
this.syncClient.sync(`${this.localPath}/dist`, this.publicBucket, {
filters: this.filters,
commandInput: input => ({
ACL: 'public-read',
ContentType: mime.lookup(input.Key),
CacheControl: input.Key.startsWith('data/')
? 'no-cache, stale-while-revalidate=5, stale-if-error=86400'
: undefined,
}),
}),
this.syncClient.sync(`${this.localPath}/storage`, this.privateBucket),
]);
}
get s3Client() {
return this._s3Client ??= new S3Client({
endpoint: process.env.AWS_S3_ENDPOINT,
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
}
/** @returns {S3SyncClient} */
get syncClient() {
return this._syncClient ??= new S3SyncClient({ client: this.s3Client });
}
get publicBucket() {
return `s3://${process.env.AWS_S3_BUCKET}`;
}
get privateBucket() {
return `s3://${process.env.AWS_S3_PRIVATE_BUCKET}`;
}
get localPath() {
return path.resolve('.');
}
get filters() {
return [
{ exclude: () => true }, // Exclude everything by default
{ include: (key) => key.startsWith('assets/splatnet/') },
{ include: (key) => key.startsWith('data/') },
{ include: (key) => key.startsWith('twitter-images/') },
];
}
log(message) {
console.log(`[S3] ${message}`);
}
}
module.exports = S3Syncer;

43
src/app/sync/index.js Normal file
View File

@ -0,0 +1,43 @@
const S3Syncer = require('./S3Syncer');
function canSync() {
return !!(
process.env.AWS_ACCESS_KEY_ID &&
process.env.AWS_SECRET_ACCESS_KEY &&
process.env.AWS_S3_BUCKET &&
process.env.AWS_S3_PRIVATE_BUCKET
);
}
async function doSync(download, upload) {
if (!canSync()) {
console.warn('Missing S3 connection parameters');
return;
}
const syncer = new S3Syncer();
if (download) {
console.info('Downloading files...');
await syncer.download();
}
if (upload) {
console.info('Uploading files...');
await syncer.upload();
}
}
function sync() {
return doSync(true, true);
}
function syncUpload() {
return doSync(false, true);
}
function syncDownload() {
return doSync(true, false);
}
module.exports = { canSync, sync, syncUpload, syncDownload };

View File

@ -31,7 +31,7 @@ class GearTweet extends TwitterPostBase {
return captureGearScreenshot(now);
}
getPublicImageFilename(data) {
getPublicImageFilename() {
return 'gear.png';
}

View File

@ -41,7 +41,7 @@ class ScheduleTweet extends TwitterPostBase {
return captureScheduleScreenshot(data.regular.start_time, this.globalSplatfestOpenInAllRegions());
}
getPublicImageFilename(data) {
getPublicImageFilename() {
return 'schedule.png';
}

View File

@ -124,14 +124,15 @@ class SplatfestTweet extends TwitterPostBase {
return `Reminder: The ${isGlobal ? 'global' : regionDemonyms} Splatfest starts in ${data.text}! #splatfest #splatoon2`;
return `Reminder: The Splatfest starts in ${this.regionInfo.name} in ${data.text}! #splatfest #splatoon2`;
case 'end':
case 'end': {
let hours = (data.festival.times.result - this.getDataTime()) / 60 / 60;
let duration = (hours == 1) ? '1 hour' : `${hours} hours`;
if (isSimultaneous)
return `The ${isGlobal ? 'global' : regionDemonyms} Splatfest is now closed. Results will be posted in ${duration}! #splatfest #splatoon2`;
return `The Splatfest is now closed in ${this.regionInfo.name}. Results will be posted in ${duration}! #splatfest #splatoon2`;
}
case 'result':
case 'result': {
let winner = data.results.summary.total ? 'bravo' : 'alpha';
// Just hardcoding this in here for now to avoid dealing with loading the Vuex store separately
@ -141,6 +142,7 @@ class SplatfestTweet extends TwitterPostBase {
let results = resultsFormat.replace('{team}', teamName);
return `${isGlobal ? 'Global' : regionDemonyms} Splatfest results: ${results} #splatfest #splatoon2`;
}
}
}
}

View File

@ -1,7 +1,7 @@
const path = require('path');
const fs = require('fs');
const mkdirp = require('mkdirp').sync;
const { postMediaTweet } = require('../client');
const { canTweet, postMediaTweet } = require('../client');
const { getTopOfCurrentHour, readJson, writeJson } = require('@/common/utilities');
const lastTweetTimesPath = path.resolve('storage/twitter-lastTweetTimes.json');
@ -19,6 +19,12 @@ class TwitterPostBase {
return false;
}
// Make sure we can post or save to a file
if (!canTweet() && !this.getPublicImageFilename()) {
this.error('Twitter API parameters not specified');
return false;
}
return this.postTweet();
}
@ -32,13 +38,15 @@ class TwitterPostBase {
// Maybe save the image
this.maybeSavePublicImage(data, image);
// Post to Twitter
let tweet = await postMediaTweet(text, image);
if (canTweet()) {
// Post to Twitter
let tweet = await postMediaTweet(text, image);
// Update the last post time
this.updateLastTweetTime();
// Update the last post time
this.updateLastTweetTime();
this.info('Posted Tweet');
this.info('Posted Tweet');
}
}
catch (e) {
this.error('Couldn\'t post Tweet');
@ -47,11 +55,12 @@ class TwitterPostBase {
}
maybeSavePublicImage(data, image) {
let filename = this.getPublicImageFilename(data);
let filename = this.getPublicImageFilename();
if (filename) {
let outputFilename = path.resolve(`dist/twitter-images/${filename}`);
mkdirp(path.dirname(outputFilename));
fs.writeFileSync(outputFilename, image);
this.info(`Saved public image as ${filename}`);
}
}
@ -155,7 +164,7 @@ class TwitterPostBase {
getImage(data) { }
// The filename to store the image as (optional)
getPublicImageFilename(data) { }
getPublicImageFilename() { }
// The text body of the Tweet
getText(data) { }

View File

@ -1,14 +1,16 @@
const { canTweet } = require('./client');
const S3Syncer = require('../sync/S3Syncer');
const { canSync } = require('../sync');
const tweets = require('./tweets');
async function maybePostTweets() {
if (!canTweet()) {
console.warn('Twitter API parameters not specified');
return;
}
const syncer = canSync() ? new S3Syncer() : null;
for (let tweet of tweets)
await tweet.maybePostTweet();
if (syncer) {
await syncer.upload();
}
}
async function testScreenshots() {

View File

@ -4,6 +4,8 @@ const TimelineUpdater = require('./updaters/TimelineUpdater');
const OriginalGearImageUpdater = require('./updaters/OriginalGearImageUpdater');
const FestivalsUpdater = require('./updaters/FestivalsUpdater');
const MerchandisesUpdater = require('./updaters/MerchandisesUpdater');
const S3Syncer = require('../sync/S3Syncer');
const { canSync } = require('../sync');
const updaters = [
new OriginalGearImageUpdater,
@ -17,6 +19,8 @@ const updaters = [
];
async function updateAll() {
const syncer = canSync() ? new S3Syncer() : null;
for (let updater of updaters) {
try {
await updater.update();
@ -25,6 +29,10 @@ async function updateAll() {
}
}
if (syncer) {
await syncer.upload();
}
return 'Done';
}

View File

@ -12,6 +12,7 @@ const LocalizationProcessor = require('../LocalizationProcessor');
const dataPath = path.resolve('dist/data');
const splatnetAssetPath = path.resolve('dist/assets/splatnet');
const cdnAltPath = path.resolve('src/common/cdn');
class Updater {
constructor(options = {}) {
@ -153,7 +154,7 @@ class Updater {
return false;
// Remove timeline items with an importance of -1
if (value.hasOwnProperty('importance'))
if ('importance' in value)
return value.importance > -1;
return true;
@ -184,6 +185,15 @@ class Updater {
if (fs.existsSync(localPath))
return;
// Certain images are not available on the CDN anymore
if (fs.existsSync(cdnAltPath + imagePath)) {
this.info(`Using CDN backup: ${imagePath}`);
let image = fs.readFileSync(cdnAltPath + imagePath);
this.writeFile(localPath, image);
return;
}
// Otherwise, download the image
this.info(`Downloading image: ${imagePath}`);
let splatnet = new SplatNet;

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -22,7 +22,7 @@ module.exports = async function retrieveGearData() {
// Find all gear and skills
let players = [battleData.player_result].concat(battleData.my_team_members, battleData.other_team_members);
for (player of players) {
for (let player of players) {
// Get the main and sub skills for each type of gear
let gearSkills = [
player.player.head_skills.main,
@ -30,13 +30,13 @@ module.exports = async function retrieveGearData() {
player.player.shoes_skills.main,
].concat(player.player.head_skills.subs, player.player.clothes_skills.subs, player.player.shoes_skills.subs);
for (skill of gearSkills) {
for (let skill of gearSkills) {
if (skill)
skills[skill.id] = skill;
}
// Get brands
for (brand of [player.player.head.brand, player.player.clothes.brand, player.player.shoes.brand])
for (let brand of [player.player.head.brand, player.player.clothes.brand, player.player.shoes.brand])
brands[brand.id] = brand;
// Get gear

View File

@ -40,6 +40,7 @@ module.exports = async () => {
// Update gear/brand/skill data from SplatNet
// (This takes a while and doesn't need to be updated frequently, so just disabling this here for now)
// eslint-disable-next-line no-constant-condition
if (false) {
let gearData = await retrieveGearData();
@ -66,6 +67,7 @@ module.exports = async () => {
let regex = /\{\{GearList\/Item.*?filter_brand/g;
let row;
// eslint-disable-next-line no-cond-assign
while (row = regex.exec(response.data)) {
// Format: name=value|brand=value|...
let details = row[0].split('|')

8
src/utility/website.json Normal file
View File

@ -0,0 +1,8 @@
{
"IndexDocument": {
"Suffix": "index.html"
},
"ErrorDocument": {
"Key": "index.html"
}
}

View File

@ -12,8 +12,7 @@
</h3>
<div class="inner-content">
<p>
Splatoon2.ink shows the current and upcoming map schedules for
<a href="https://www.amazon.com/gp/product/B01N9QVIRV/ref=as_li_tl?ie=UTF8&amp;tag=matisesblo-20&amp;camp=1789&amp;creative=9325&amp;linkCode=as2&amp;creativeASIN=B01N9QVIRV&amp;linkId=01d5f076af5b6008436d9843c40a28db" target="_blank">Splatoon 2</a>.
Splatoon2.ink shows the current and upcoming map schedules for Splatoon 2.
</p>
<p>
This site was built with <a href="https://vuejs.org/" target="_blank">Vue.js</a>
@ -36,14 +35,6 @@
If you'd like to use this site's data in your own site/bot/app, please review the data access policy
<a href="https://github.com/misenhower/splatoon2.ink/wiki/Data-access-policy" target="_blank">here</a>.
</p>
<h5 class="font-splatoon2 title is-5">
Donations
</h5>
<p>
If you'd like to support ongoing development and maintenance of this site, please consider
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7KETJ7PSE5PQC" target="_blank">making a donation</a>.
Thanks :)
</p>
<p>
<a href="https://twitter.com/mattisenhower" target="_blank">Follow me on Twitter</a>
or <a href="mailto:matt@isenhower.com" target="_blank">email me</a> with any questions!

View File

@ -72,7 +72,7 @@ function generateModule(region) {
allSplatfests(state, getters, rootState, rootGetters) {
return rootGetters['splatoon/splatfests/allSplatfests']
&& rootGetters['splatoon/splatfests/allSplatfests']
.filter(s => s.regions.hasOwnProperty(region)) // Only show Splatfests for this region
.filter(s => region in s.regions) // Only show Splatfests for this region
.map(s => ({ ...s, ...s.regions[region] })); // Apply this region's data
},
currentSplatfest(state, getters, { splatoon }) {

9925
yarn.lock

File diff suppressed because it is too large Load Diff