hfbcast 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,127 @@
1
+ import * as fs from 'fs';
2
+ import * as url from 'url';
3
+ import { AppDataSource } from "./data-source";
4
+ import {Site} from "./entity/Site";
5
+ import {Broadcast} from "./entity/Broadcast";
6
+ import {JSDOM} from "jsdom";
7
+ const jquery = require("jquery");
8
+
9
+ function parseBroadcasts(sourceName) {
10
+
11
+ return new Promise(resolve => {
12
+
13
+ const broadcasts = [];
14
+ const sites = [];
15
+
16
+ fs.readFile(`../sources/shortwave_${sourceName}.html`, 'utf8', async function(err, testHTML) {
17
+
18
+ global.document = new JSDOM( testHTML );
19
+ const $ = jquery(global.document.window);
20
+ const rows = $('table.results tr');
21
+
22
+ rows.each(async (i, row) => {
23
+
24
+ // if (i===0 || i>100) return false;
25
+ if (i===0) return false;
26
+ if (i%1000 === 0) console.log(sourceName, i);
27
+
28
+ const cells = $(row).find('td');
29
+
30
+ const fq = Number.parseFloat($(cells[0]).html());
31
+ const startStopTime = $(cells[1]).html().split('-');
32
+ const station = $(cells[2]).html();
33
+ const country = $(cells[3]).html();
34
+ const language = $(cells[4]).html();
35
+ const days = $(cells[5]).html();
36
+ const link = $(cells[6]).html();
37
+ let lon=0, lat=0, siteName='unknown';
38
+ try {
39
+ const a = $(link);
40
+ // console.log(a.length);
41
+ siteName = a.length === 1 ? a.html() : link;
42
+ const coordString = (new url.URL(a.attr('href'))).searchParams.get('center').split(',');
43
+ lat = Number.parseFloat(coordString[0]);
44
+ lon = Number.parseFloat(coordString[1]);
45
+ }
46
+ catch (e) {
47
+ }
48
+
49
+ const startHour = Number.parseInt(startStopTime[0].substring(0,2));
50
+ const startMin = Number.parseInt(startStopTime[0].substring(2,4));
51
+ const stopHour = Number.parseInt(startStopTime[1].substring(0,2));
52
+ const stopMin = Number.parseInt(startStopTime[1].substring(2,4));
53
+
54
+ let daysInteger = 0;
55
+ for (let i = 0; i < days.length; i++) {
56
+ const dayInt = parseInt(days.charAt(i));
57
+ if (!isNaN(dayInt)) {
58
+ daysInteger += Math.pow(2, dayInt-1);
59
+ }
60
+ }
61
+
62
+ // console.log(fq, lon, lat, startStopTime, startHour, stopHour, days);
63
+
64
+ const site = new Site();
65
+ site.name = siteName;
66
+ site.power = 0;
67
+ site.lon = lon;
68
+ site.lat = lat;
69
+
70
+ const broadcast = new Broadcast();
71
+ broadcast.frequency = fq;
72
+ broadcast.site = site;
73
+ broadcast.days = daysInteger;
74
+ broadcast.startTime = startMin + 60*startHour;
75
+ broadcast.endTime = stopMin + 60*stopHour;
76
+ broadcast.station = station;
77
+ broadcast.country = country;
78
+ broadcast.language = language;
79
+ broadcast.source = sourceName;
80
+
81
+ sites.push(site);
82
+ broadcasts.push(broadcast);
83
+ })
84
+
85
+ resolve(broadcasts);
86
+ });
87
+ })
88
+ }
89
+
90
+ AppDataSource.initialize().then(async () => {
91
+
92
+ console.log('PARSING HTML ...');
93
+
94
+ const broadcasts1: any = await parseBroadcasts('AOKI');
95
+ console.log('...', broadcasts1.length, 'BROADCASTS FOUND IN AOKI');
96
+ const broadcasts2: any = await parseBroadcasts('EIBI');
97
+ console.log('...', broadcasts2.length, 'BROADCASTS FOUND IN EIBI');
98
+ const broadcasts3: any = await parseBroadcasts('HFCC');
99
+ console.log('...', broadcasts3.length, 'BROADCASTS FOUND IN HFCC');
100
+
101
+ const broadcasts = [... broadcasts1, ... broadcasts2, ... broadcasts3]
102
+
103
+ console.log(broadcasts.length, 'BROADCASTS FOUND IN TOTAL');
104
+
105
+ console.log('POPULATING DATABASE ...');
106
+
107
+ for (let i=0; i<broadcasts.length; i++) {
108
+ try {
109
+ if (i%1000 === 0) console.log(i);
110
+ // await siteRepository.save(broadcasts[i].site);
111
+
112
+ const siteName = broadcasts[i].site.name;
113
+ const site = await AppDataSource.manager.findOne(Site, {where: {name: siteName}});
114
+ if (site) {
115
+ broadcasts[i].site = site;
116
+ }
117
+ else {
118
+ await AppDataSource.manager.save(broadcasts[i].site);
119
+ }
120
+ await AppDataSource.manager.save(broadcasts[i]);
121
+ }
122
+ catch (e) {
123
+ console.log(i, 'NOT UNIQUE', broadcasts[i]);
124
+ }
125
+ }
126
+
127
+ }).catch(error => console.log(error))
@@ -0,0 +1,17 @@
1
+ import "reflect-metadata"
2
+ import { DataSource } from "typeorm"
3
+ import { Site } from "./entity/Site"
4
+ import { Broadcast } from "./entity/Broadcast"
5
+
6
+ const currentTime = new Date();
7
+
8
+ export const AppDataSource = new DataSource({
9
+ type: "sqlite",
10
+ // database: `database_${currentTime.getFullYear()}-${currentTime.getMonth()}-${currentTime.getDate()}_${currentTime.getHours()}${currentTime.getMinutes()}${currentTime.getSeconds()}.sqlite`,
11
+ database: `database.sqlite`,
12
+ synchronize: true,
13
+ logging: false,
14
+ entities: [Site, Broadcast],
15
+ migrations: [],
16
+ subscribers: [],
17
+ })
@@ -0,0 +1,38 @@
1
+ import {Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique} from "typeorm";
2
+ import {Site} from "./Site";
3
+
4
+ @Entity()
5
+ @Unique(['frequency', 'days', 'startTime', 'endTime', 'station', 'country', 'language', 'site'])
6
+ export class Broadcast {
7
+
8
+ @PrimaryGeneratedColumn()
9
+ id: number;
10
+
11
+ @Column({nullable: true})
12
+ frequency: number;
13
+
14
+ @Column({nullable: true})
15
+ days: number;
16
+
17
+ @Column({nullable: true})
18
+ startTime: number;
19
+
20
+ @Column({nullable: true})
21
+ endTime: number;
22
+
23
+ @Column({nullable: true})
24
+ station: string;
25
+
26
+ @Column({nullable: true})
27
+ country: string;
28
+
29
+ @Column({nullable: true})
30
+ language: string;
31
+
32
+ @Column({nullable: true})
33
+ source: string;
34
+
35
+ @ManyToOne(type => Site)
36
+ @JoinColumn()
37
+ site: Site;
38
+ }
@@ -0,0 +1,26 @@
1
+ import {Entity, PrimaryGeneratedColumn, Column, Unique, OneToMany} from "typeorm";
2
+ import {Broadcast} from "./Broadcast";
3
+
4
+ @Entity()
5
+ @Unique(['name'])
6
+ export class Site {
7
+
8
+ @PrimaryGeneratedColumn()
9
+ id: number;
10
+
11
+ @Column()
12
+ name: string;
13
+
14
+ @Column()
15
+ power: number;
16
+
17
+ @Column()
18
+ lon: number;
19
+
20
+ @Column()
21
+ lat: number;
22
+
23
+ @OneToMany(type => Broadcast, broadcasts => broadcasts.site)
24
+ broadcasts: Broadcast[];
25
+
26
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "sqlite",
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@types/node": "^18.13.0",
7
+ "jsdom": "^21.1.0",
8
+ "reflect-metadata": "^0.1.13",
9
+ "sqlite3": "^5.1.4",
10
+ "ts-node": "^10.9.1",
11
+ "typeorm": "^0.3.12",
12
+ "typescript": "^4.9.5"
13
+ },
14
+ "scripts": {
15
+ "build": "ts-node build.ts"
16
+ }
17
+ }
package/src/demo.html ADDED
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Title</title>
6
+ </head>
7
+ <body style="font-family: monospace">
8
+ database url ......... <input value="http://localhost:3333/database.sqlite.gz" style="width:300px"><br>
9
+ min frequency (kHz) .. <input value="0"><br>
10
+ max frequency (kHz) .. <input value="30000"><br><br>
11
+ <button>get broadcasts</button><br>
12
+ </body>
13
+ </html>
package/src/demo.js ADDED
@@ -0,0 +1,6 @@
1
+ import {BroadcastDb} from "hfbcast"
2
+
3
+ (new BroadcastDb('database.sqlite.gz')).then(db => {
4
+ const result = db.getBroadcastsBetween(0,30000);
5
+ console.log(result);
6
+ })
package/src/index.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A broadcast database entry
3
+ */
4
+ declare type broadcastEntry = {
5
+ values: any[],
6
+ columns: any[]
7
+ }
8
+
9
+ /**
10
+ * A database class for HF broadcasts
11
+ */
12
+ export declare class BroadcastDb {
13
+
14
+ /**
15
+ * Constructor
16
+ * @param {string} gzUrl URL of a gzipped sqlite3 database file
17
+ */
18
+ constructor();
19
+
20
+ /**
21
+ * Get broadcasts between two frequencies given in KHz
22
+ * @param {number} minFreqkHz minimum frequency
23
+ * @param {number} maxFreqkHz maximum frequency
24
+ */
25
+ getBroadcastsBetween(minFreqkHz: number, maxFreqkHz: number): broadcastEntry[];
26
+
27
+ /**
28
+ * Get broadcasts in an interval around at a center frequency in KHz
29
+ * @param {number} frequencykHz center frequency
30
+ * @param {number} intervalkHz frequency interval
31
+ */
32
+ getBroadcastsAt(frequencyKHz: number, intervalKHz: number): broadcastEntry[];
33
+ }
package/src/index.js ADDED
@@ -0,0 +1,83 @@
1
+ import dumm from "sql.js";
2
+ import pako from "pako";
3
+ import sqlwasm from 'url-loader!sql.js/dist/sql-wasm.wasm'
4
+ import gzUrl from "file-loader?name=database.sqlite.gz!../assets/database.sqlite.gz";
5
+
6
+ async function openDb(url) {
7
+ return new Promise(resolve => {
8
+ fetch(sqlwasm).then(res => res.blob()).then(async blob => {
9
+ const sqlPromise = dumm({
10
+ locateFile: () => URL.createObjectURL(blob)
11
+ });
12
+ const dataPromise = fetch(url).then(res => res.arrayBuffer());
13
+ const [SQL, buf] = await Promise.all([sqlPromise, dataPromise])
14
+ const db = new SQL.Database(new Uint8Array(pako.ungzip(buf)));
15
+ resolve(db);
16
+ })
17
+ })
18
+ }
19
+
20
+ export class BroadcastDb {
21
+
22
+ constructor() {
23
+ return new Promise(async resolve => {
24
+ this.db = await openDb(gzUrl);
25
+ resolve(this)
26
+ })
27
+ }
28
+
29
+ getBroadcastsBetween(minFreqkHz, maxFreqkHz) {
30
+
31
+ const now = new Date();
32
+ const currentDay = now.getUTCDay();
33
+ const currentHour = now.getUTCHours();
34
+ const currentMinute = now.getUTCMinutes();
35
+ const currentTime = currentMinute + 60*currentHour;
36
+
37
+ const query = `SELECT * FROM broadcast
38
+ JOIN site on broadcast.siteId = site.id
39
+ WHERE broadcast.startTime <= ${currentTime} AND
40
+ broadcast.endTime >= ${currentTime} AND
41
+ NOT (site.lat = 0 AND site.lon = 0) AND
42
+ broadcast.days & (1 << ${currentDay}) != 0 AND
43
+ broadcast.frequency <= ${maxFreqkHz} AND
44
+ broadcast.frequency >= ${minFreqkHz}
45
+ `
46
+ const res = this.db.exec(query);
47
+
48
+ return res[0];
49
+ }
50
+
51
+ getBroadcastsAt(frequencyKHz, intervalKHz) {
52
+
53
+ const now = new Date();
54
+ const currentDay = now.getUTCDay();
55
+ const currentHour = now.getUTCHours();
56
+ const currentMinute = now.getUTCMinutes();
57
+ const currentTime = currentMinute + 60*currentHour;
58
+
59
+ const query = `SELECT * FROM broadcast
60
+ JOIN site on broadcast.siteId = site.id
61
+ WHERE broadcast.startTime <= ${currentTime} AND
62
+ broadcast.endTime >= ${currentTime} AND
63
+ NOT (site.lat = 0 AND site.lon = 0) AND
64
+ broadcast.days & (1 << ${currentDay}) != 0 AND
65
+ broadcast.frequency <= ${intervalKHz + frequencyKHz/1000} AND
66
+ broadcast.frequency >= ${-intervalKHz + frequencyKHz/1000}
67
+ `
68
+
69
+ const broadcasts = [];
70
+ const res = this.db.exec(query);
71
+ if (res.length > 0) {
72
+ res[0].values.forEach(c => {
73
+ const broadcast = {};
74
+ c.forEach((colvalue, i) => {
75
+ broadcast[res[0].columns[i]] = colvalue;
76
+ })
77
+ broadcasts.push(broadcast);
78
+ })
79
+ }
80
+
81
+ return broadcasts;
82
+ }
83
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "es5",
5
+ "es6"
6
+ ],
7
+ "target": "es5",
8
+ "module": "commonjs",
9
+ "moduleResolution": "node",
10
+ "outDir": "./build",
11
+ "emitDecoratorMetadata": true,
12
+ "experimentalDecorators": true,
13
+ "sourceMap": true
14
+ }
15
+ }
@@ -0,0 +1,28 @@
1
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
2
+
3
+ module.exports = {
4
+ mode: 'development',
5
+ entry: {
6
+ rfdfhfbroadcast: './src/index.js',
7
+ demo: './src/demo.js'
8
+ },
9
+ output: {
10
+ filename: '[name].js',
11
+ library: 'rfdfhfbroadcast',
12
+ libraryTarget: 'umd'
13
+ },
14
+ resolve: {
15
+ extensions: ['.tsx', '.ts', '.js'],
16
+ fallback: { crypto: false, path: false, fs: false, util: false }
17
+ },
18
+ devServer: {
19
+ port: 4444,
20
+ static: ['assets']
21
+ },
22
+ plugins: [
23
+ new HtmlWebpackPlugin({
24
+ chunks: ['demo'],
25
+ template: "./src/demo.html"
26
+ })
27
+ ],
28
+ }