Version 6.5.4

This commit is contained in:
Chrystian Huot 2022-09-09 11:52:20 -04:00
parent bf75f92f47
commit d03f0b1fef
22 changed files with 400 additions and 258 deletions

View file

@ -31,6 +31,14 @@ _v6.5.3_
- Add filename to dirwatch error messages (issue #248).
- Dirwatch.type=default now defaults to the current date and time if none are provided by the metatags (discussion #250).
_v6.5.4_
- Fixed some warnings when linting server code.
- New dirwatch type `DSDPlus Fast Lane` (discussion #244).
- Added new error catches on dirwatch (issue 254).
- Fixed search by inaccurate time (issue #258).
- Reverted sync.Map to regular map with sync.Mutex.
## Version 6.4
- New `-cmd` command line options to allow advanced administrative tasks.

View file

@ -27,13 +27,13 @@ Clone the official repository on your computer and start the build process.
When finished, you will find the precompiled versions for various platforms in the `dist` folder.
rdio-scanner-darwin-amd64-v6.5.3.zip
rdio-scanner-darwin-arm64-v6.5.3.zip
rdio-scanner-freebsd-amd64-v6.5.3.zip
rdio-scanner-linux-386-v6.5.3.zip
rdio-scanner-linux-amd64-v6.5.3.zip
rdio-scanner-linux-arm64-v6.5.3.zip
rdio-scanner-linux-arm-v6.5.3.zip
rdio-scanner-windows-amd64-v6.5.3.zip
rdio-scanner-darwin-amd64-v6.5.4.zip
rdio-scanner-darwin-arm64-v6.5.4.zip
rdio-scanner-freebsd-amd64-v6.5.4.zip
rdio-scanner-linux-386-v6.5.4.zip
rdio-scanner-linux-amd64-v6.5.4.zip
rdio-scanner-linux-arm64-v6.5.4.zip
rdio-scanner-linux-arm-v6.5.4.zip
rdio-scanner-windows-amd64-v6.5.4.zip
**Happy Rdio scanning !**

View file

@ -16,8 +16,8 @@
################################################################################
app := rdio-scanner
date := 2022/08/10
ver := 6.5.3
date := 2022/09/09
ver := 6.5.4
client := $(wildcard client/*.json client/*.ts)
server := $(wildcard server/*.go)

View file

@ -16,9 +16,10 @@ Here is a list of recorders known to work with [Rdio Scanner](https://github.com
| -------------------------------------------------------------- | --- | -------- |
| [Trunk Recorder](https://github.com/robotastic/trunk-recorder) | X | X |
| [RTLSDR-Airband](https://github.com/szpajder/RTLSDR-Airband) | | X |
| [sdrtrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [SDRTrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [voxcall](https://github.com/aaknitt/voxcall) | X | |
| [ProScan](https://www.proscan.org/) | | X |
| [DSDPlus Fast Lane](https://https://www.dsdplus.com/) | | X |
# Quick start

View file

@ -1,12 +1,12 @@
{
"name": "rdio-scanner",
"version": "6.5.3",
"version": "6.5.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "rdio-scanner",
"version": "6.5.2",
"version": "6.5.4",
"license": "LICENSE",
"dependencies": {
"@angular/animations": "^13.3.10",

View file

@ -38,5 +38,5 @@
"build": "ng build --base-href ./ --configuration production",
"start": "ng serve"
},
"version": "6.5.3"
"version": "6.5.4"
}

View file

@ -637,6 +637,10 @@ export class RdioScannerAdminService implements OnDestroy {
return null;
}
if (control.value.startsWith('\\')) {
return { network: true }
}
const dirWatch: DirWatch[] = control.parent?.parent?.getRawValue() || [];
const count = dirWatch.reduce((c, a) => c += a.directory === control.value ? 1 : 0, 0);
@ -653,7 +657,7 @@ export class RdioScannerAdminService implements OnDestroy {
const type = dirwatch.type;
return ['sdr-trunk'].includes(type) || control.value !== null || /#SYS/.test(mask) ? null : { required: true };
return ['dsdplus', 'sdr-trunk'].includes(type) || control.value !== null || /#SYS/.test(mask) ? null : { required: true };
};
}
@ -665,7 +669,7 @@ export class RdioScannerAdminService implements OnDestroy {
const type = dirwatch.type;
return ['trunk-recorder', 'sdr-trunk'].includes(type) || control.value !== null || /#TG/.test(mask) ? null : { required: true };
return ['dsdplus', 'trunk-recorder', 'sdr-trunk'].includes(type) || control.value !== null || /#TG/.test(mask) ? null : { required: true };
};
}

View file

@ -33,6 +33,28 @@
<mat-slide-toggle color="primary" formControlName="deleteAfter"></mat-slide-toggle>
</div>
</div>
<div class="row">
<p>
<span class="mat-body">Type</span><br>
<span class="mat-caption">
Dirwatch type defines how the metadata are obtained.
<ul>
<li><b>Default</b> - Extract the metadata from a custom mask.</li>
<li><b>DSDPlus Fast Lane</b> - Extract the metadata from the file path.</li>
<li><b>Trunk Recorder</b> - Extract the metadata from the json file.</li>
<li><b>SDR Trunk</b> - Extract the metadata from the MP3 tags defined on the SDR Trunk's aliases tab.</li>
</ul>
</span>
</p>
<mat-form-field floatLabel="never">
<mat-select formControlName="type" placeholder="Type">
<mat-option value="default">Default</mat-option>
<mat-option value="dsdplus">DSDPlus Fast Lane</mat-option>
<mat-option value="trunk-recorder">Trunk Recorder</mat-option>
<mat-option value="sdr-trunk">SDR Trunk</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="row">
<p>
<span class="mat-body">Directory</span><br>
@ -49,9 +71,12 @@
<mat-error *ngIf="dirWatch.get('directory')?.hasError('duplicate')">
Directory is already defined
</mat-error>
<mat-error *ngIf="dirWatch.get('directory')?.hasError('network')">
Incompatible network folder
</mat-error>
</mat-form-field>
</div>
<div class="row">
<div class="row" *ngIf="['default','dsdplus','trunk-recorder'].includes(dirWatch.get('type')?.value)">
<p>
<span class="mat-body">Extension</span><br>
<span class="mat-caption">The audio call extension to monitor without the period. Ex.: "mp3",
@ -64,29 +89,14 @@
</mat-error>
</mat-form-field>
</div>
<div class="row">
<p>
<span class="mat-body">Type</span><br>
<span class="mat-caption">When SDR Trunk, metadata are obtained from the MP3 tags. Note that
the label of the SDR Trunk system must match the label of the Rdio Scanner system label. When
trunk-recorder, metadata are obtained from the JSON file.</span>
</p>
<mat-form-field floatLabel="never">
<mat-select formControlName="type" placeholder="Type">
<mat-option [value]="null">Default</mat-option>
<mat-option value="trunk-recorder">Trunk Recorder</mat-option>
<mat-option value="sdr-trunk">SDR Trunk (aliases tab only)</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="row">
<div class="row" *ngIf="['default','dsdplus'].includes(dirWatch.get('type')?.value)">
<p>
<span class="mat-body">System</span><br>
<span class="mat-caption">System to where the audio files should go.</span>
</p>
<mat-form-field floatLabel="never">
<mat-select #sysId formControlName="systemId" placeholder="System">
<mat-option [value]="null">Metatag #SYS</mat-option>
<mat-option [value]="null"></mat-option>
<mat-option *ngFor="let system of systems" [value]="system.value.id">
{{ system.value.label }}
</mat-option>
@ -96,14 +106,14 @@
</mat-error>
</mat-form-field>
</div>
<div class="row">
<div class="row" *ngIf="['default'].includes(dirWatch.get('type')?.value)">
<p>
<span class="mat-body">Talkgroup</span><br>
<span class="mat-caption">Talkgroup to where the audio files should go.</span>
</p>
<mat-form-field floatLabel="never">
<mat-select formControlName="talkgroupId" placeholder="Talkgroup">
<mat-option [value]="null">Metatag #TG</mat-option>
<mat-option [value]="null"></mat-option>
<mat-option *ngFor="let talkgroup of talkgroups[dirWatch.value.systemId] || []"
[value]="talkgroup.value.id">
{{ talkgroup.value.label }}
@ -114,7 +124,7 @@
</mat-error>
</mat-form-field>
</div>
<div class="row">
<div class="row" *ngIf="['default'].includes(dirWatch.get('type')?.value)">
<p>
<span class="mat-body">Mask</span><br>
<span class="mat-caption">Some metadata can be extracted from the file name of the audio file using
@ -156,7 +166,7 @@
</mat-error>
</mat-form-field>
</div>
<div class="row">
<div class="row" *ngIf="['default'].includes(dirWatch.get('type')?.value)">
<p>
<span class="mat-body">Frequency</span><br>
<span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span>
@ -168,7 +178,7 @@
</mat-error>
</mat-form-field>
</div>
<div class="row">
<div class="row" *ngIf="['default'].includes(dirWatch.get('type')?.value)">
<p>
<span class="mat-body">Delay</span><br>
<span class="mat-caption">Depending on the recorder, audio files can be ingested too soon after the

View file

@ -53,12 +53,12 @@ $ docker run --detach --env TZ=America/Toronto --name rdio-scanner --publish 300
520cdbf51fca11d8bacea12d81245f1cb4d984f80d2be2e3039727b59533a6a9
$ docker logs rdio-scanner
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 08:57:23 server started
2022/08/10 08:57:23 base folder is /app/data
2022/08/10 08:57:23 main interface at http://6d87e8dc37a0:3000
2022/08/10 08:57:23 admin interface at http://6d87e8dc37a0:3000/admin
2022/09/09 08:57:23 server started
2022/09/09 08:57:23 base folder is /app/data
2022/09/09 08:57:23 main interface at http://6d87e8dc37a0:3000
2022/09/09 08:57:23 admin interface at http://6d87e8dc37a0:3000/admin
```
You can also use docker-compose to start the server with this simple `docker-compose.yml`:

View file

@ -18,9 +18,10 @@ Here is a list of recorders known to work with [Rdio Scanner](https://github.com
| -------------------------------------------------------------- | --- | -------- |
| [Trunk Recorder](https://github.com/robotastic/trunk-recorder) | X | X |
| [RTLSDR-Airband](https://github.com/szpajder/RTLSDR-Airband) | | X |
| [sdrtrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [SDRTrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [voxcall](https://github.com/aaknitt/voxcall) | X | |
| [ProScan](https://www.proscan.org/) | | X |
| [DSDPlus Fast Lane](https://https://www.dsdplus.com/) | | X |
# Improve your experience on the go
@ -42,8 +43,8 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
rdio@macos ~ % mkdir rdio-scanner
rdio@macos ~ % cd rdio-scanner
rdio@macos rdio-scanner % unzip \
> ~/Downloads/rdio-scanner-darwin-arm64-v6.5.3.zip
Archive: /Users/rdio/Downloads/rdio-scanner-darwin-arm64-v6.5.3.zip
> ~/Downloads/rdio-scanner-darwin-arm64-v6.5.4.zip
Archive: /Users/rdio/Downloads/rdio-scanner-darwin-arm64-v6.5.4.zip
inflating: rdio-scanner
inflating: rdio-scanner.pdf
@ -51,11 +52,11 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
rdio@macos rdio-scanner % ./rdio-scanner
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 08:38:06 server started
2022/08/10 08:38:06 main interface at http://macos.local:3000
2022/08/10 08:38:06 admin interface at http://macos.local:3000/admin
2022/09/09 08:38:06 server started
2022/09/09 08:38:06 main interface at http://macos.local:3000
2022/09/09 08:38:06 admin interface at http://macos.local:3000/admin
4. Access the administrative dashboard to finalize the configuration.
@ -77,11 +78,11 @@ Here we want our [Rdio Scanner](https://guthub.com/chuot/rdio-scanner) instance
rdio@macos rdio-scanner % ./rdio-scanner --listen :80
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 08:48:03 server started
2022/08/10 08:48:03 main interface at http://macos.local
2022/08/10 08:48:03 admin interface at http://macos.local/admin
2022/09/09 08:48:03 server started
2022/09/09 08:48:03 main interface at http://macos.local
2022/09/09 08:48:03 admin interface at http://macos.local/admin
## Listening on a SSL port
@ -95,12 +96,12 @@ You can use your own SSL certificates that match your domain name with `-ssl_cer
> -ssl_key_file mykey.key \
> -ssl_listen :443
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 08:50:58 server started
2022/08/10 08:50:58 main interface at http://macos.local
2022/08/10 08:50:58 main interface at https://macos.local
2022/08/10 08:50:58 admin interface at https://macos.local/admin
2022/09/09 08:50:58 server started
2022/09/09 08:50:58 main interface at http://macos.local
2022/09/09 08:50:58 main interface at https://macos.local
2022/09/09 08:50:58 admin interface at https://macos.local/admin
If you don't want to worry about SSL certificates, you can use the built-in Let's Encrypt auto-cert feature. This requires that you have both port 80 (HTTP) and port 443 (HTTPS) open to the world. Also, your domain name should point to your IP address where [Rdio Scanner](https://github.com/chuot/rdio-scanner/) is running. The advantage of this approach is that everything is done automatically, no certificate request, no certificate renewal.
@ -118,7 +119,7 @@ You don't want to have to type everytime a long list of arguments. No problem, y
> -ssl_auto_cert mydomain.com \
> -ssl_listen :443 \
> -config_save
2022/08/10 08:52:24 rdio-scanner.ini file created
2022/09/09 08:52:24 rdio-scanner.ini file created
All of your parameters passed as arguments to [Rdio Scanner](https://github.com/chuot/rdio-scanner) have been saved to an INI file which has the same arguments/values list.
@ -132,12 +133,12 @@ Then simply run [Rdio Scanner](https://github.com/chuot/rdio-scanner) without an
rdio@macos rdio-scanner % ./rdio-scanner
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 08:54:08 server started
2022/08/10 08:54:08 main interface at http://macos.local
2022/08/10 08:54:08 main interface at https://macos.local
2022/08/10 08:54:08 admin interface at https://macos.local/admin
2022/09/09 08:54:08 server started
2022/09/09 08:54:08 main interface at http://macos.local
2022/09/09 08:54:08 main interface at https://macos.local
2022/09/09 08:54:08 admin interface at https://macos.local/admin
## Install Rdio Scanner as a service

View file

@ -18,9 +18,10 @@ Here is a list of recorders known to work with [Rdio Scanner](https://github.com
| -------------------------------------------------------------- | --- | -------- |
| [Trunk Recorder](https://github.com/robotastic/trunk-recorder) | X | X |
| [RTLSDR-Airband](https://github.com/szpajder/RTLSDR-Airband) | | X |
| [sdrtrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [SDRTrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [voxcall](https://github.com/aaknitt/voxcall) | X | |
| [ProScan](https://www.proscan.org/) | | X |
| [DSDPlus Fast Lane](https://https://www.dsdplus.com/) | | X |
# Improve your experience on the go
@ -42,8 +43,8 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
rdio@pc-freebsd:~ $ mkdir rdio-scanner
rdio@pc-freebsd:~ $ cd rdio-scanner
rdio@pc-freebsd:~/rdio-scanner $ unzip \
> ~/rdio-scanner-freebsd-amd64-v6.5.3.zip
Archive: ../rdio-scanner-freebsd-amd64-v6.5.3.zip
> ~/rdio-scanner-freebsd-amd64-v6.5.4.zip
Archive: ../rdio-scanner-freebsd-amd64-v6.5.4.zip
extracting: rdio-scanner
extracting: rdio-scanner.pdf
@ -51,11 +52,11 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
rdio@pc-freebsd:~/rdio-scanner $ ./rdio-scanner
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 08:16:36 server started
2022/08/10 08:16:36 main interface at http://pc-freebsd:3000
2022/08/10 08:16:36 admin interface at http://pc-freebsd:3000/admin
2022/09/09 08:16:36 server started
2022/09/09 08:16:36 main interface at http://pc-freebsd:3000
2022/09/09 08:16:36 admin interface at http://pc-freebsd:3000/admin
4. Access the administrative dashboard to finalize the configuration.
@ -81,11 +82,11 @@ Here we want our [Rdio Scanner](https://guthub.com/chuot/rdio-scanner) instance
Password:
root@pc-freebsd:/home/rdio/rdio-scanner # ./rdio-scanner -listen :80
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 08:19:38 server started
2022/08/10 08:19:38 main interface at http://pc-freebsd
2022/08/10 08:19:38 admin interface at http://pc-freebsd/admin
2022/09/09 08:19:38 server started
2022/09/09 08:19:38 main interface at http://pc-freebsd
2022/09/09 08:19:38 admin interface at http://pc-freebsd/admin
## Listening on a SSL port
@ -101,12 +102,12 @@ You can use your own SSL certificates that match your domain name with `-ssl_cer
> -ssl_key_file mykey.key \
> -ssl_listen :443
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 10:34:29 server started
2022/08/10 10:34:29 main interface at http://pc-freebsd
2022/08/10 10:34:29 main interface at https://pc-freebsd
2022/08/10 10:34:29 admin interface at https://pc-freebsd/admin
2022/09/09 10:34:29 server started
2022/09/09 10:34:29 main interface at http://pc-freebsd
2022/09/09 10:34:29 main interface at https://pc-freebsd
2022/09/09 10:34:29 admin interface at https://pc-freebsd/admin
If you don't want to worry about SSL certificates, you can use the built-in Let's Encrypt auto-cert feature. This requires that you have both port 80 (HTTP) and port 443 (HTTPS) open to the world. Also, your domain name should point to your IP address where [Rdio Scanner](https://github.com/chuot/rdio-scanner/) is running. The advantage of this approach is that everything is done automatically, no certificate request, no certificate renewal.
@ -126,7 +127,7 @@ You don't want to have to type everytime a long list of arguments. No problem, y
> -ssl_auto_cert mydomain.com \
> -ssl_listen :443 \
> -config_save
2022/08/10 10:37:00 rdio-scanner.ini file created
2022/09/09 10:37:00 rdio-scanner.ini file created
All of your parameters passed as arguments to [Rdio Scanner](https://github.com/chuot/rdio-scanner) have been saved to an INI file which has the same arguments/values list.
@ -142,12 +143,12 @@ Then simply run [Rdio Scanner](https://github.com/chuot/rdio-scanner) without an
Password:
root@pc-freebsd:/home/rdio/rdio-scanner # ./rdio-scanner
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 10:38:28 server started
2022/08/10 10:38:29 main interface at http://pc-freebsd
2022/08/10 10:38:29 main interface at https://pc-freebsd
2022/08/10 10:38:29 admin interface at https://pc-freebsd/admin
2022/09/09 10:38:28 server started
2022/09/09 10:38:29 main interface at http://pc-freebsd
2022/09/09 10:38:29 main interface at https://pc-freebsd
2022/09/09 10:38:29 admin interface at https://pc-freebsd/admin
## Install Rdio Scanner as a service

View file

@ -18,9 +18,10 @@ Here is a list of recorders known to work with [Rdio Scanner](https://github.com
| -------------------------------------------------------------- | --- | -------- |
| [Trunk Recorder](https://github.com/robotastic/trunk-recorder) | X | X |
| [RTLSDR-Airband](https://github.com/szpajder/RTLSDR-Airband) | | X |
| [sdrtrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [SDRTrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [voxcall](https://github.com/aaknitt/voxcall) | X | |
| [ProScan](https://www.proscan.org/) | | X |
| [DSDPlus Fast Lane](https://https://www.dsdplus.com/) | | X |
# Improve your experience on the go
@ -42,8 +43,8 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
[rdio@pc-linux ~]$ mkdir rdio-scanner
[rdio@pc-linux ~]$ cd rdio-scanner
[rdio@pc-linux rdio-scanner]$ unzip \
> ~/Downloads/rdio-scanner-linux-amd64-v6.5.3.zip
Archive: /home/rdio/Downloads/rdio-scanner-linux-amd64-v6.5.3.zip
> ~/Downloads/rdio-scanner-linux-amd64-v6.5.4.zip
Archive: /home/rdio/Downloads/rdio-scanner-linux-amd64-v6.5.4.zip
inflating: rdio-scanner
inflating: rdio-scanner.pdf
@ -51,11 +52,11 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
[rdio@pc-linux rdio-scanner]$ ./rdio-scanner
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 09:11:48 server started
2022/08/10 09:11:48 main interface at http://pc-linux:3000
2022/08/10 09:11:48 admin interface at http://pc-linux:3000/admin
2022/09/09 09:11:48 server started
2022/09/09 09:11:48 main interface at http://pc-linux:3000
2022/09/09 09:11:48 admin interface at http://pc-linux:3000/admin
4. Access the administrative dashboard to finalize the configuration.
@ -78,11 +79,11 @@ Here we want our [Rdio Scanner](https://guthub.com/chuot/rdio-scanner) instance
[rdio@pc-linux rdio-scanner]$ sudo ./rdio-scanner -listen :80
[sudo] password for rdio:
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 09:14:00 server started
2022/08/10 09:14:00 main interface at http://pc-linux
2022/08/10 09:14:00 admin interface at http://pc-linux/admin
2022/09/09 09:14:00 server started
2022/09/09 09:14:00 main interface at http://pc-linux
2022/09/09 09:14:00 admin interface at http://pc-linux/admin
## Listening on a SSL port
@ -97,12 +98,12 @@ You can use your own SSL certificates that match your domain name with `-ssl_cer
> -ssl_listen :443
[sudo] password for rdio:
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 09:16:47 server started
2022/08/10 09:16:47 main interface at http://pc-linux
2022/08/10 09:16:47 main interface at https://pc-linux
2022/08/10 09:16:47 admin interface at https://pc-linux/admin
2022/09/09 09:16:47 server started
2022/09/09 09:16:47 main interface at http://pc-linux
2022/09/09 09:16:47 main interface at https://pc-linux
2022/09/09 09:16:47 admin interface at https://pc-linux/admin
If you don't want to worry about SSL certificates, you can use the built-in Let's Encrypt auto-cert feature. This requires that you have both port 80 (HTTP) and port 443 (HTTPS) open to the world. Also, your domain name should point to your IP address where [Rdio Scanner](https://github.com/chuot/rdio-scanner/) is running. The advantage of this approach is that everything is done automatically, no certificate request, no certificate renewal.
@ -120,7 +121,7 @@ You don't want to have to type everytime a long list of arguments. No problem, y
> -ssl_auto_cert mydomain.com \
> -ssl_listen :443 \
> -config_save
2022/08/10 09:19:29 rdio-scanner.ini file created
2022/09/09 09:19:29 rdio-scanner.ini file created
All of your parameters passed as arguments to [Rdio Scanner](https://github.com/chuot/rdio-scanner) have been saved to an INI file which has the same arguments/values list.
@ -135,12 +136,12 @@ Then simply run [Rdio Scanner](https://github.com/chuot/rdio-scanner) without an
[rdio@pc-linux rdio-scanner]$ sudo ./rdio-scanner
[sudo] Mot de passe de rdio :
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 09:20:40 server started
2022/08/10 09:20:40 main interface at http://pc-linux
2022/08/10 09:20:40 main interface at https://pc-linux
2022/08/10 09:20:40 admin interface at https://pc-linux/admin
2022/09/09 09:20:40 server started
2022/09/09 09:20:40 main interface at http://pc-linux
2022/09/09 09:20:40 main interface at https://pc-linux
2022/09/09 09:20:40 admin interface at https://pc-linux/admin
## Install Rdio Scanner as a service

View file

@ -18,9 +18,10 @@ Here is a list of recorders known to work with [Rdio Scanner](https://github.com
| -------------------------------------------------------------- | --- | -------- |
| [Trunk Recorder](https://github.com/robotastic/trunk-recorder) | X | X |
| [RTLSDR-Airband](https://github.com/szpajder/RTLSDR-Airband) | | X |
| [sdrtrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [SDRTrunk](https://github.com/DSheirer/sdrtrunk) | | X |
| [voxcall](https://github.com/aaknitt/voxcall) | X | |
| [ProScan](https://www.proscan.org/) | | X |
| [DSDPlus Fast Lane](https://https://www.dsdplus.com/) | | X |
# Improve your experience on the go
@ -44,7 +45,7 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
C:\Users\rdio> cd rdio-scanner
C:\Users\rdio\rdio-scanner> tar -xvf ^
..\downloads\rdio-scanner-windows-amd64-v6.5.3.zip
..\downloads\rdio-scanner-windows-amd64-v6.5.4.zip
x rdio-scanner.exe
x rdio-scanner.pdf
@ -52,11 +53,11 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
C:\Users\rdio\rdio-scanner>rdio-scanner
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 13:48:48 server started
2022/08/10 13:48:48 main interface at http://pc-windows:3000
2022/08/10 13:48:48 admin interface at http://pc-windows:3000/admin
2022/09/09 13:48:48 server started
2022/09/09 13:48:48 main interface at http://pc-windows:3000
2022/09/09 13:48:48 admin interface at http://pc-windows:3000/admin
4. Access the administrative dashboard to finalize the configuration.
@ -78,11 +79,11 @@ Here we want our [Rdio Scanner](https://guthub.com/chuot/rdio-scanner) instance
C:\Users\rdio\rdio-scanner>rdio-scanner -listen :80
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 10:53:31 server started
2022/08/10 10:53:31 main interface at http://pc-windows
2022/08/10 10:53:31 admin interface at http://pc-windows/admin
2022/09/09 10:53:31 server started
2022/09/09 10:53:31 main interface at http://pc-windows
2022/09/09 10:53:31 admin interface at http://pc-windows/admin
## Listening on a SSL port
@ -96,12 +97,12 @@ You can use your own SSL certificates that match your domain name with `-ssl_cer
-ssl_key_file meykey.key ^
-ssl_listen :443
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 11:05:43 server started
2022/08/10 11:05:43 main interface at http://pc-windows
2022/08/10 11:05:43 main interface at https://pc-windows
2022/08/10 11:05:43 admin interface at https://pc-windows/admin
2022/09/09 11:05:43 server started
2022/09/09 11:05:43 main interface at http://pc-windows
2022/09/09 11:05:43 main interface at https://pc-windows
2022/09/09 11:05:43 admin interface at https://pc-windows/admin
If you don't want to worry about SSL certificates, you can use the built-in Let's Encrypt auto-cert feature. This requires that you have both port 80 (HTTP) and port 443 (HTTPS) open to the world. Also, your domain name should point to your IP address where [Rdio Scanner](https://github.com/chuot/rdio-scanner/) is running. The advantage of this approach is that everything is done automatically, no certificate request, no certificate renewal.
@ -119,7 +120,7 @@ You don't want to have to type everytime a long list of arguments. No problem, y
-ssl_auto_cert mydomain.com ^
-ssl_listen :443 ^
-config_save
2022/08/10 11:08:28 rdio-scanner.ini file created
2022/09/09 11:08:28 rdio-scanner.ini file created
All of your parameters passed as arguments to [Rdio Scanner](https://github.com/chuot/rdio-scanner) have been saved to an INI file which has the same arguments/values list.
@ -133,12 +134,12 @@ Then simply run [Rdio Scanner](https://github.com/chuot/rdio-scanner) without an
C:\Users\rdio\rdio-scanner>rdio-scanner
Rdio Scanner v6.5.3
Rdio Scanner v6.5.4
----------------------------------
2022/08/10 11:11:28 server started
2022/08/10 11:11:28 main interface at http://pc-windows
2022/08/10 11:11:28 main interface at https://pc-windows
2022/08/10 11:11:28 admin interface at https://pc-windows/admin
2022/09/09 11:11:28 server started
2022/09/09 11:11:28 main interface at http://pc-windows
2022/09/09 11:11:28 main interface at https://pc-windows
2022/09/09 11:11:28 admin interface at https://pc-windows/admin
\pagebreak{}

View file

@ -338,6 +338,8 @@ func (calls *Calls) Search(searchOptions *CallsSearchOptions, client *Client) (*
if t, err = db.ParseDateTime(dateTime); err == nil {
searchResults.DateStop = t
} else {
searchResults.DateStop = time.Now()
}
switch v := searchOptions.Sort.(type) {
@ -364,8 +366,8 @@ func (calls *Calls) Search(searchOptions *CallsSearchOptions, client *Client) (*
stop = start.Add(time.Hour*24 - time.Millisecond)
} else {
start = time.Date(v.Year(), v.Month(), v.Day(), v.Hour(), v.Minute(), 0, 0, time.UTC).Add(time.Hour*-24 - time.Duration(v.Hour())).Add(time.Minute * time.Duration(-v.Minute()))
stop = start.Add(time.Hour*24 - time.Millisecond - time.Duration(v.Hour())).Add(time.Minute * time.Duration(-v.Minute()))
start = time.Date(v.Year(), v.Month(), v.Day(), v.Hour(), v.Minute(), 0, 0, time.UTC).Add(time.Hour*-24 + time.Millisecond)
stop = time.Date(v.Year(), v.Month(), v.Day(), v.Hour(), v.Minute(), 0, 0, time.UTC)
}
where += fmt.Sprintf(" and (`dateTime` between '%v' and '%v')", start.Format(df), stop.Format(df))

View file

@ -206,91 +206,90 @@ func (client *Client) SendListenersCount(count int) {
}
type Clients struct {
Map sync.Map
count int
Map map[*Client]bool
mutex sync.Mutex
}
func NewClients() *Clients {
return &Clients{
Map: sync.Map{},
count: 0,
Map: map[*Client]bool{},
mutex: sync.Mutex{},
}
}
func (clients *Clients) AccessCount(client *Client) int {
clients.mutex.Lock()
defer clients.mutex.Unlock()
count := 0
clients.Map.Range(func(k any, _ any) bool {
switch c := k.(type) {
case *Client:
if c.Access == client.Access {
count++
}
for c := range clients.Map {
if c.Access == client.Access {
count++
}
return true
})
}
return count
}
func (clients *Clients) Add(client *Client) {
clients.count = clients.count + 1
clients.Map.Store(client, true)
clients.mutex.Lock()
defer clients.mutex.Unlock()
clients.Map[client] = true
}
func (clients *Clients) Count() int {
return clients.count
clients.mutex.Lock()
defer clients.mutex.Unlock()
return len(clients.Map)
}
func (clients *Clients) EmitCall(call *Call, restricted bool) {
clients.Map.Range(func(k any, _ any) bool {
switch c := k.(type) {
case *Client:
if (!restricted || c.Access.HasAccess(call)) && c.Livefeed.IsEnabled(call) {
c.Send <- &Message{Command: MessageCommandCall, Payload: call}
}
}
clients.mutex.Lock()
defer clients.mutex.Unlock()
return true
})
for c := range clients.Map {
if (!restricted || c.Access.HasAccess(call)) && c.Livefeed.IsEnabled(call) {
c.Send <- &Message{Command: MessageCommandCall, Payload: call}
}
}
}
func (clients *Clients) EmitConfig(groups *Groups, options *Options, systems *Systems, tags *Tags, restricted bool) {
count := clients.Count()
clients.mutex.Lock()
defer clients.mutex.Unlock()
clients.Map.Range(func(k any, _ any) bool {
switch c := k.(type) {
case *Client:
if restricted {
c.Send <- &Message{Command: MessageCommandPin}
} else {
c.SendConfig(groups, options, systems, tags)
}
count := len(clients.Map)
if options.ShowListenersCount {
c.SendListenersCount(count)
}
for c := range clients.Map {
if restricted {
c.Send <- &Message{Command: MessageCommandPin}
} else {
c.SendConfig(groups, options, systems, tags)
}
return true
})
if options.ShowListenersCount {
c.SendListenersCount(count)
}
}
}
func (clients *Clients) EmitListenersCount() {
count := clients.Count()
clients.mutex.Lock()
defer clients.mutex.Unlock()
clients.Map.Range(func(k any, _ any) bool {
switch c := k.(type) {
case *Client:
c.SendListenersCount(count)
}
count := len(clients.Map)
return true
})
for c := range clients.Map {
c.SendListenersCount(count)
}
}
func (clients *Clients) Remove(client *Client) {
if _, loaded := clients.Map.LoadAndDelete(client); loaded {
clients.count = clients.count - 1
}
clients.mutex.Lock()
defer clients.mutex.Unlock()
delete(clients.Map, client)
}

View file

@ -138,7 +138,7 @@ func (command *Command) Do(action string) {
command.url = readVal()
if err != nil || !regexp.MustCompile(`^https?://`).Match([]byte(command.url)) {
command.exitWithError(errors.New("Invalid URL"))
command.exitWithError(errors.New("invalid URL"))
}
}
@ -259,7 +259,7 @@ func (command *Command) configGet() {
}
}
default:
command.exitWithError(errors.New("Invalid response"))
command.exitWithError(errors.New("invalid response"))
}
} else {
command.exitWithError(err)
@ -314,10 +314,10 @@ func (command *Command) login() {
}
fmt.Println("Logged in.")
default:
command.exitWithError(errors.New("No token in response"))
command.exitWithError(errors.New("no token in response"))
}
default:
command.exitWithError(errors.New("Invalid response"))
command.exitWithError(errors.New("invalid response"))
}
} else {
command.exitWithError(err)

View file

@ -23,7 +23,6 @@ import (
"os"
"os/signal"
"strconv"
"sync"
"time"
)
@ -48,7 +47,6 @@ type Controller struct {
Register chan *Client
Unregister chan *Client
Ingest chan *Call
ingestMutex sync.Mutex
running bool
}
@ -271,7 +269,7 @@ func (controller *Controller) IngestCall(call *Call) {
}
if system == nil || talkgroup == nil {
logCall(call, LogLevelInfo, "no matching system/talkgroup")
logCall(call, LogLevelWarn, "no matching system/talkgroup")
return
}
@ -516,7 +514,7 @@ func (controller *Controller) Start() error {
}
go func() {
c := make(chan os.Signal)
c := make(chan os.Signal, 8)
signal.Notify(c, os.Interrupt)
<-c
controller.Terminate()
@ -536,7 +534,10 @@ func (controller *Controller) Start() error {
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(time.Second, func() {
timer = nil
controller.LogClientsCount()
if controller.Options.ShowListenersCount {

View file

@ -21,7 +21,6 @@ import (
"errors"
"fmt"
"io/fs"
"io/ioutil"
"math"
"mime"
"os"
@ -38,9 +37,10 @@ import (
)
const (
DirwatchKindDefault = "default"
DirwatchKindSdrTrunk = "sdr-trunk"
DirwatchKindTrunkRecorder = "trunk-recorder"
DirwatchTypeDefault = "default"
DirwatchTypeDSDPlus = "dsdplus"
DirwatchTypeSdrTrunk = "sdr-trunk"
DirwatchTypeTrunkRecorder = "trunk-recorder"
)
type Dirwatch struct {
@ -59,9 +59,19 @@ type Dirwatch struct {
UsePolling bool `json:"usePolling"`
controller *Controller
dirs map[string]bool
mutex sync.Mutex
timers map[string]*time.Timer
watcher *fsnotify.Watcher
}
func NewDirwatch() *Dirwatch {
return &Dirwatch{
dirs: map[string]bool{},
mutex: sync.Mutex{},
timers: map[string]*time.Timer{},
}
}
func (dirwatch *Dirwatch) FromMap(m map[string]any) *Dirwatch {
switch v := m["_id"].(type) {
case float64:
@ -135,16 +145,18 @@ func (dirwatch *Dirwatch) Ingest(p string) {
var err error
switch dirwatch.Kind {
case DirwatchKindTrunkRecorder:
case DirwatchTypeDSDPlus:
err = dirwatch.ingestDSDPlus(p)
case DirwatchTypeTrunkRecorder:
err = dirwatch.ingestTrunkRecorder(p)
case DirwatchKindSdrTrunk:
case DirwatchTypeSdrTrunk:
err = dirwatch.ingestSdrTrunk(p)
default:
err = dirwatch.ingestDefault(p)
}
if err != nil {
dirwatch.controller.Logs.LogEvent(LogLevelError, fmt.Sprintf("dirwatch.ingest: %s, %s", err.Error(), p))
dirwatch.controller.Logs.LogEvent(LogLevelWarn, fmt.Sprintf("dirwatch.ingest: %s, %s", err.Error(), p))
}
}
@ -156,7 +168,11 @@ func (dirwatch *Dirwatch) ingestDefault(p string) error {
switch v := dirwatch.Extension.(type) {
case string:
ext = fmt.Sprintf(".%s", v)
if len(v) > 0 {
ext = fmt.Sprintf(".%s", v)
} else {
ext = ".wav"
}
default:
ext = ".wav"
}
@ -202,7 +218,7 @@ func (dirwatch *Dirwatch) ingestDefault(p string) error {
return err
}
func (dirwatch *Dirwatch) ingestSdrTrunk(p string) error {
func (dirwatch *Dirwatch) ingestDSDPlus(p string) error {
var (
err error
ext string
@ -229,6 +245,48 @@ func (dirwatch *Dirwatch) ingestSdrTrunk(p string) error {
call.AudioType = mime.TypeByExtension(path.Ext(p))
call.Frequency = dirwatch.Frequency
switch v := dirwatch.SystemId.(type) {
case uint:
call.System = v
}
if call.Audio, err = os.ReadFile(p); err != nil {
return err
}
if err = ParseDSDPlusMeta(call, p); err != nil {
return err
}
if ok, err := call.IsValid(); ok {
dirwatch.controller.Ingest <- call
if dirwatch.DeleteAfter {
if err = os.Remove(p); err != nil {
return err
}
}
} else {
return err
}
return nil
}
func (dirwatch *Dirwatch) ingestSdrTrunk(p string) error {
var err error
if !strings.EqualFold(path.Ext(p), ".mp3") {
return nil
}
call := NewCall()
call.AudioName = filepath.Base(p)
call.AudioType = mime.TypeByExtension(path.Ext(p))
call.Frequency = dirwatch.Frequency
if call.Audio, err = os.ReadFile(p); err != nil {
return err
}
@ -290,11 +348,11 @@ func (dirwatch *Dirwatch) ingestTrunkRecorder(p string) error {
call.System = v
}
if call.Audio, err = ioutil.ReadFile(audioName); err != nil {
if call.Audio, err = os.ReadFile(audioName); err != nil {
return nil
}
if b, err = ioutil.ReadFile(p); err != nil {
if b, err = os.ReadFile(p); err != nil {
return err
}
@ -550,15 +608,16 @@ func (dirwatch *Dirwatch) Start(controller *Controller) error {
}
go func() {
var timers = sync.Map{}
logError := func(err error) {
controller.Logs.LogEvent(LogLevelError, fmt.Sprintf("dirwatch.watcher: %v", err.Error()))
}
newTimer := func(eventName string) *time.Timer {
return time.AfterFunc(delay, func() {
timers.Delete(eventName)
dirwatch.mutex.Lock()
defer dirwatch.mutex.Unlock()
delete(dirwatch.timers, eventName)
if _, err := os.Stat(eventName); err == nil {
dirwatch.Ingest(eventName)
@ -567,74 +626,71 @@ func (dirwatch *Dirwatch) Start(controller *Controller) error {
}
defer func() {
timers.Range(func(k any, t any) bool {
switch v := t.(type) {
case *time.Timer:
v.Stop()
}
timers.Delete(k)
return true
})
switch v := recover().(type) {
case error:
controller.Logs.LogEvent(LogLevelError, v.Error())
}
dirwatch.mutex.Lock()
defer dirwatch.mutex.Unlock()
for e, t := range dirwatch.timers {
t.Stop()
delete(dirwatch.timers, e)
}
if dirwatch.watcher != nil {
dirwatch.Start(controller)
}
}()
for {
if dirwatch.watcher == nil {
break
return
}
select {
case event, ok := <-dirwatch.watcher.Events:
if ok {
switch event.Op {
case fsnotify.Create:
if dirwatch.isDir(event.Name) {
if err := dirwatch.walkDir(event.Name); err != nil {
logError(err)
}
if !ok {
return
}
} else {
if f, ok := timers.LoadAndDelete(event.Name); ok {
switch v := f.(type) {
case *time.Timer:
v.Stop()
}
}
timers.Store(event.Name, newTimer(event.Name))
switch event.Op {
case fsnotify.Create:
if dirwatch.isDir(event.Name) {
if err := dirwatch.walkDir(event.Name); err != nil {
logError(err)
}
case fsnotify.Remove:
if dirwatch.dirs[event.Name] {
if err := dirwatch.watcher.Remove(event.Name); err == nil {
delete(dirwatch.dirs, event.Name)
} else {
logError(err)
}
} else {
if dirwatch.timers[event.Name] != nil {
dirwatch.timers[event.Name].Stop()
}
case fsnotify.Write:
if f, ok := timers.LoadAndDelete(event.Name); ok {
switch v := f.(type) {
case *time.Timer:
v.Stop()
}
}
timers.Store(event.Name, newTimer(event.Name))
dirwatch.timers[event.Name] = newTimer(event.Name)
}
case fsnotify.Remove:
if dirwatch.dirs[event.Name] {
if err := dirwatch.watcher.Remove(event.Name); err == nil {
delete(dirwatch.dirs, event.Name)
} else {
logError(err)
}
}
case fsnotify.Write:
if dirwatch.timers[event.Name] != nil {
dirwatch.timers[event.Name].Stop()
}
dirwatch.timers[event.Name] = newTimer(event.Name)
}
case err, ok := <-dirwatch.watcher.Errors:
if ok {
logError(err)
return
}
return
}
}
}()
@ -671,8 +727,9 @@ func (dirwatch *Dirwatch) Start(controller *Controller) error {
func (dirwatch *Dirwatch) Stop() {
if dirwatch.watcher != nil {
dirwatch.watcher.Close()
w := dirwatch.watcher
dirwatch.watcher = nil
w.Close()
}
}
@ -699,8 +756,7 @@ func (dirwatches *Dirwatches) FromMap(f []any) *Dirwatches {
for _, f := range f {
switch v := f.(type) {
case map[string]any:
dirwatch := &Dirwatch{}
dirwatch.FromMap(v)
dirwatch := NewDirwatch().FromMap(v)
dirwatches.List = append(dirwatches.List, dirwatch)
}
}
@ -739,7 +795,7 @@ func (dirwatches *Dirwatches) Read(db *Database) error {
}
for rows.Next() {
dirwatch := &Dirwatch{}
dirwatch := NewDirwatch()
if err = rows.Scan(&id, &delay, &dirwatch.DeleteAfter, &dirwatch.Directory, &dirwatch.Disabled, &extension, &frequency, &mask, &order, &systemId, &talkgroupId, &kind, &dirwatch.UsePolling); err != nil {
break

View file

@ -75,7 +75,7 @@ func (ffmpeg *FFMpeg) Convert(call *Call, systems *Systems, tags *Tags, mode uin
if !ffmpeg.warned {
ffmpeg.warned = true
return errors.New("ffmpeg is not available, no audio conversion will be performed.")
return errors.New("ffmpeg is not available, no audio conversion will be performed")
}
return nil
}

View file

@ -18,7 +18,7 @@ package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"io"
"log"
"mime"
"net/http"
@ -198,7 +198,7 @@ func main() {
TLSConfig: tlsConfig,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
ErrorLog: log.New(ioutil.Discard, "", 0),
ErrorLog: log.New(io.Discard, "", 0),
}
s.SetKeepAlivesEnabled(true)

View file

@ -21,13 +21,70 @@ import (
"mime"
"mime/multipart"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/dhowden/tag"
)
func ParseDSDPlusMeta(call *Call, fp string) error {
dir := filepath.Dir(fp)
base := strings.TrimSuffix(filepath.Base(fp), filepath.Ext(fp))
if d := regexp.MustCompile(`([0-9]+)$`).FindStringSubmatch(dir); len(d) == 2 && len(d[1]) == 8 {
if t := regexp.MustCompile(`^([0-9]+)`).FindStringSubmatch(base); len(t) == 2 && len(t[1]) == 6 {
if dy, err := strconv.Atoi(d[1][0:4]); err == nil {
if dm, err := strconv.Atoi(d[1][4:6]); err == nil {
if dd, err := strconv.Atoi(d[1][6:8]); err == nil {
if th, err := strconv.Atoi(t[1][0:2]); err == nil {
if tm, err := strconv.Atoi(t[1][2:4]); err == nil {
if ts, err := strconv.Atoi(t[1][4:6]); err == nil {
call.DateTime = time.Date(dy, time.Month(dm), dd, th, tm, ts, 0, time.Now().Location()).UTC()
}
}
}
}
}
}
}
}
if s := regexp.MustCompile(`^[0-9]+_[0-9]+_[^_]+_([0-9]+)-([0-9]+)_`).FindStringSubmatch(base); len(s) == 3 {
if sys, err := strconv.Atoi(s[1]); err == nil && sys > 0 {
call.System = uint(sys)
}
}
tu := regexp.MustCompile(`([0-9]+)_([0-9]+)$|([0-9]+)_([0-9]+)\[([^\]]*)\]$`).FindStringSubmatch(base)
if len(tu[3]) > 0 {
tu[1] = tu[3]
}
if len(tu[4]) > 0 {
tu[2] = tu[4]
}
if tg, err := strconv.Atoi(tu[1]); err == nil && tg > 0 {
call.Talkgroup = uint(tg)
if src, err := strconv.Atoi(tu[2]); err == nil && src > 0 {
call.Source = uint(src)
if len(tu[5]) > 0 && strings.TrimSpace(tu[5]) != strings.TrimSpace(tu[2]) {
if b := regexp.MustCompile(`^([\.\-\ ,_]+)$`).MatchString(tu[5]); !b {
units := NewUnits()
units.Add(uint(src), tu[5])
call.units = units
}
}
}
}
return nil
}
func ParseSdrTrunkMeta(call *Call, controller *Controller) error {
var (
s []string
@ -43,7 +100,7 @@ func ParseSdrTrunkMeta(call *Call, controller *Controller) error {
s = regexp.MustCompile(`^([0-9]+) ?(.*)$`).FindStringSubmatch(m.Artist())
if len(s) >= 2 {
if i, err = strconv.Atoi(s[1]); err != nil {
if i, err = strconv.Atoi(s[1][0:1]); err != nil {
return err
}
if i > 0 {

View file

@ -15,4 +15,4 @@
package main
const Version = "6.5.3"
const Version = "6.5.4"