init
43
backend/app.js
Normal file
@@ -0,0 +1,43 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var photosearch = require('./routes/photosearch');
|
||||
|
||||
var app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use('/search/photos', photosearch)
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function(req, res, next) {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
90
backend/bin/www
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('backend:server');
|
||||
var http = require('http');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
||||
89
backend/lib/anki.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { readFile } from 'fs/promises'
|
||||
import { open } from 'sqlite'
|
||||
import sqlite3 from 'sqlite3'
|
||||
import AdmZip from 'adm-zip'
|
||||
|
||||
// database structure: https://github.com/ankidroid/Anki-Android/wiki/Database-Structure
|
||||
// python example: https://github.com/patarapolw/ankisync2
|
||||
// old js example: https://github.com/nornagon/mkanki/blob/master/index.js
|
||||
|
||||
// to create a zip file
|
||||
// const zip = new AdmZip()
|
||||
// zip.addLocalFolder('./folder_to_zip')
|
||||
// zip.writeZip('./output.zip')
|
||||
|
||||
class Deck {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param info ID of internal deck, or filename or anki export
|
||||
* @param process whether or not this is an external anki export to process
|
||||
*/
|
||||
constructor(info, process) {
|
||||
// pull out variables
|
||||
this.notes = null // this is what is edited; may create multiple cards
|
||||
this.media = null // image and audio files attached to cards/notes
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an internal deck
|
||||
*/
|
||||
async load(id) {
|
||||
// open database
|
||||
// open media files
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an anki export for internal use. Currently only supports
|
||||
* .apkg files. In the future, may support:
|
||||
* colpkg
|
||||
* txt (notes)
|
||||
* txt (cards)
|
||||
*
|
||||
* @param filename path of file to process
|
||||
*/
|
||||
async process(filename) {
|
||||
return await this.processApkg(filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a .apkg file into a database
|
||||
*
|
||||
* @param filename name of file to open and process
|
||||
*/
|
||||
async processApkg(filename) {
|
||||
// error handling
|
||||
if (!filename) throw new Error('Must specify a .apkg filename')
|
||||
|
||||
// setup
|
||||
let uuid = crypto.randomUUID()
|
||||
let location = `../data/anki/decks/${uuid}/`
|
||||
|
||||
// make the resource folder if it does not exist already
|
||||
// let resource = ''
|
||||
// if (!fs.existsSync(resource)) fs.mkdirSync(resource, { recursive: true })
|
||||
|
||||
// extract zip
|
||||
// https://stuk.github.io/jszip/documentation/howto/read_zip.html#in-nodejs
|
||||
let zip = new AdmZip(filename)
|
||||
zip.extractAllTo(location, true)
|
||||
|
||||
// extract the notes database
|
||||
let db = await open({
|
||||
filename: location + 'collection.anki2',
|
||||
driver: sqlite3.cached.Database
|
||||
})
|
||||
|
||||
let result = await db.get('SELECT sfld FROM notes WHERE id = :n', 1486785585103)
|
||||
|
||||
console.log(result);
|
||||
|
||||
// extract media files
|
||||
// ? process/rename media files and update in db?
|
||||
// load data into this obj
|
||||
|
||||
// return { notes, media }
|
||||
}
|
||||
}
|
||||
|
||||
export default Deck
|
||||
BIN
backend/lib/test.apkg
Normal file
4
backend/lib/test.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import Deck from './anki.js'
|
||||
|
||||
let deck = new Deck()
|
||||
await deck.process('test.apkg');
|
||||
BIN
backend/lib/test/0
Normal file
BIN
backend/lib/test/1
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/lib/test/10
Normal file
BIN
backend/lib/test/11
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/lib/test/12
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
backend/lib/test/13
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
backend/lib/test/14
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
backend/lib/test/15
Normal file
BIN
backend/lib/test/16
Normal file
BIN
backend/lib/test/17
Normal file
BIN
backend/lib/test/18
Normal file
BIN
backend/lib/test/19
Normal file
BIN
backend/lib/test/2
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/lib/test/20
Normal file
BIN
backend/lib/test/21
Normal file
BIN
backend/lib/test/22
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
backend/lib/test/23
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
backend/lib/test/24
Normal file
BIN
backend/lib/test/25
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
backend/lib/test/26
Normal file
BIN
backend/lib/test/27
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/lib/test/28
Normal file
BIN
backend/lib/test/29
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/lib/test/3
Normal file
BIN
backend/lib/test/30
Normal file
BIN
backend/lib/test/31
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
backend/lib/test/32
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
backend/lib/test/33
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
backend/lib/test/34
Normal file
BIN
backend/lib/test/35
Normal file
BIN
backend/lib/test/36
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
backend/lib/test/37
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/lib/test/38
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/lib/test/39
Normal file
BIN
backend/lib/test/4
Normal file
BIN
backend/lib/test/40
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/lib/test/41
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/lib/test/42
Normal file
BIN
backend/lib/test/43
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/lib/test/44
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
backend/lib/test/45
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
backend/lib/test/46
Normal file
BIN
backend/lib/test/47
Normal file
BIN
backend/lib/test/48
Normal file
BIN
backend/lib/test/49
Normal file
BIN
backend/lib/test/5
Normal file
BIN
backend/lib/test/50
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
backend/lib/test/51
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
backend/lib/test/52
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/lib/test/53
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
backend/lib/test/54
Normal file
BIN
backend/lib/test/55
Normal file
BIN
backend/lib/test/56
Normal file
BIN
backend/lib/test/57
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
backend/lib/test/58
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
backend/lib/test/59
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
backend/lib/test/6
Normal file
BIN
backend/lib/test/60
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
backend/lib/test/61
Normal file
BIN
backend/lib/test/62
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
backend/lib/test/63
Normal file
BIN
backend/lib/test/64
Normal file
BIN
backend/lib/test/65
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
backend/lib/test/66
Normal file
BIN
backend/lib/test/67
Normal file
BIN
backend/lib/test/68
Normal file
BIN
backend/lib/test/69
Normal file
BIN
backend/lib/test/7
Normal file
BIN
backend/lib/test/70
Normal file
BIN
backend/lib/test/71
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
backend/lib/test/72
Normal file
BIN
backend/lib/test/73
Normal file
BIN
backend/lib/test/74
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/lib/test/75
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
backend/lib/test/8
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/lib/test/9
Normal file
BIN
backend/lib/test/collection.anki2
Normal file
1
backend/lib/test/media
Normal file
@@ -0,0 +1 @@
|
||||
{"66": "rec1486791173.mp3", "12": "paste-5909874999552.jpg", "49": "rec1486790484.mp3", "54": "rec1486789453.mp3", "59": "paste-6051608920308.jpg", "75": "paste-7176890351876.jpg", "50": "paste-5952824672506.jpg", "11": "paste-5342939316470.jpg", "3": "rec1486790515.mp3", "73": "rec1486791444.mp3", "58": "paste-6743098654966.jpg", "26": "rec1486791092.mp3", "60": "paste-5596342386925.jpg", "68": "rec1486787151.mp3", "69": "rec1486790743.mp3", "0": "rec1486791250.mp3", "8": "paste-6219112644848.jpg", "18": "rec1486790648.mp3", "57": "paste-6880537608452.jpg", "61": "rec1486789557.mp3", "33": "paste-6343666696438.jpg", "7": "rec1486791037.mp3", "21": "rec1486786900.mp3", "44": "paste-5476083302645.jpg", "17": "rec1486790321.mp3", "9": "rec1486790979.mp3", "55": "rec1486790707.mp3", "64": "rec1486790888.mp3", "71": "paste-7563437408511.jpg", "53": "paste-6532645257453.jpg", "36": "paste-6322191859961.jpg", "31": "paste-7687991460090.jpg", "28": "rec1486790193.mp3", "47": "rec1486791121.mp3", "39": "rec1486791213.mp3", "14": "paste-7022271529203.jpg", "37": "paste-7288559501563.jpg", "15": "rec1486789738.mp3", "51": "paste-2929167696123.jpg", "25": "paste-7597797146867.jpg", "20": "rec1486790550.mp3", "19": "rec1486790446.mp3", "48": "rec1486788047.mp3", "74": "paste-5849745457397.jpg", "30": "rec1486791007.mp3", "10": "rec1486788946.mp3", "29": "paste-6846177870075.jpg", "4": "rec1486790287.mp3", "56": "rec1486789680.mp3", "62": "paste-5755256176881.jpg", "38": "paste-2143188680961.jpg", "70": "rec1486790804.mp3", "42": "rec1486790858.mp3", "43": "paste-7430293422343.jpg", "23": "paste-6158983102706.jpg", "72": "rec1486789502.mp3", "34": "rec1486790589.mp3", "16": "rec1486791310.mp3", "52": "paste-6266357285108.jpg", "32": "paste-687194767612.jpg", "65": "paste-6708738916592.jpg", "2": "paste-7142530613495.jpg", "40": "paste-816043786497.jpg", "1": "paste-1090921693442.jpg", "67": "rec1486791391.mp3", "5": "rec1486790677.mp3", "6": "rec1486791362.mp3", "46": "rec1486790948.mp3", "22": "paste-7073811136753.jpg", "63": "rec1486790918.mp3", "24": "rec1486790620.mp3", "27": "paste-4939212390648.jpg", "45": "paste-6644314407145.jpg", "41": "paste-5811090751727.jpg", "35": "rec1486789528.mp3", "13": "paste-6442450944247.jpg"}
|
||||
1
backend/lib/utils.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
23
backend/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node ./bin/www",
|
||||
"reinstall": "rm -rf node_modules/ package-lock.json && npm install"
|
||||
},
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.13.5",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"fs": "^0.0.1-security",
|
||||
"http-errors": "~1.6.3",
|
||||
"jade": "~1.11.0",
|
||||
"morgan": "~1.9.1",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
}
|
||||
}
|
||||
9
backend/routes/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
123
backend/routes/photosearch.js
Normal file
@@ -0,0 +1,123 @@
|
||||
var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
const axios = require('axios')
|
||||
|
||||
/**
|
||||
* Services to add:
|
||||
* - https://unsplash.com/developers
|
||||
* - https://pixabay.com/api/docs/
|
||||
*
|
||||
* Pronounciation will require $2/mo sub:
|
||||
* https://api.forvo.com/plans-and-pricing/
|
||||
*/
|
||||
|
||||
async function pexelsSearch(query) {
|
||||
// https://help.pexels.com/hc/en-us/articles/47678194141337-Can-I-change-the-search-language-when-using-the-Pexels-API
|
||||
// pexels has a monthly rate limit and an hourly rate limit, only the monthly is returned in the headers
|
||||
// limit is also 200 per hour, will need to manually track that
|
||||
|
||||
const resp = await axios.get(
|
||||
`https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=5`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: process.env.PEXELS_API,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const { data, headers } = resp
|
||||
const photos = data.photos
|
||||
|
||||
let filtered = photos.map((p) => (
|
||||
{
|
||||
url: p.src.medium,
|
||||
desc: p.alt,
|
||||
credit: p.photographer,
|
||||
id: p.id
|
||||
}
|
||||
))
|
||||
|
||||
let response = {
|
||||
photos: filtered,
|
||||
ratelimit: {
|
||||
limit: headers['x-ratelimit-limit'],
|
||||
remaining: headers['x-ratelimit-remaining'],
|
||||
reset: new Date(Number(headers['x-ratelimit-reset']) * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function shutterstockSearch(query) {
|
||||
|
||||
// shutterstock has an hourly rate limit and no monthly limit
|
||||
// https://api-reference.shutterstock.com/?shell#assets-api
|
||||
// higher quality is available with shutterstock watermarks, look into [photo].assets objects
|
||||
|
||||
const SHUTTERSTOCK_API_TOKEN = process.env.SHUTTERSTOCK_API
|
||||
|
||||
try {
|
||||
const resp = await axios.get(
|
||||
'https://api.shutterstock.com/v2/images/search',
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${SHUTTERSTOCK_API_TOKEN}`
|
||||
},
|
||||
params: {
|
||||
query,
|
||||
per_page: 5,
|
||||
sort: 'popular',
|
||||
safe: false,
|
||||
image_type: 'photo'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let { data, headers } = resp
|
||||
|
||||
let filtered = data.data.map((p) => (
|
||||
{
|
||||
url: p.assets.mosaic.url,
|
||||
desc: p.description,
|
||||
credit: p.contributor.id,
|
||||
id: Number(p.id)
|
||||
}
|
||||
))
|
||||
|
||||
let response = {
|
||||
photos: filtered,
|
||||
ratelimit: {
|
||||
limit: headers['ratelimit-limit'],
|
||||
remaining: headers['ratelimit-remaining'],
|
||||
reset: new Date(Number(headers['ratelimit-reset']))
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}``
|
||||
}
|
||||
|
||||
router.get('/', async function(req, res) {
|
||||
try {
|
||||
|
||||
const queryL1 = req.query.queries[0]
|
||||
// const queryL2 = req.query.queries[1]
|
||||
|
||||
// const pexels = await pexelsSearch(queryL1)
|
||||
const shutterstock = await shutterstockSearch(queryL1)
|
||||
|
||||
response = shutterstock;
|
||||
|
||||
res.status(200).send(response)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router
|
||||
9
backend/routes/users.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
/* GET users listing. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.status(200).send('respond with a resource');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||