├── .github └── workflows │ ├── backend.yml │ ├── frontend.yml │ └── grabbers.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── dev ├── QrssAnalysis │ ├── Program.cs │ ├── QrssAnalysis.csproj │ └── QrssAnalysis.sln ├── banner │ ├── banner.jpg │ ├── banner.png │ └── banner.psd ├── execute.http ├── icon │ ├── icon.ico │ ├── icon.png │ └── make_icon.bat └── sample-images │ ├── 7L1RLL-136KHZ 2021.05.31.19.22.00 618x439 .jpg │ ├── CT2IWW 2021.05.31.19.22.00 1122x692 .jpg │ ├── DL4DTL 2021.05.31.19.22.00 1122x692 .jpg │ ├── EA8BVP-30M-FAST 2021.05.31.19.22.00 1107x648 .jpg │ ├── EA8BVP-30M-SLOW 2021.05.31.19.02.00 1104x647 .jpg │ ├── G0MQW-1 2021.05.31.19.22.00 1000x747 .jpg │ ├── G0MQW-2 2021.05.31.19.22.00 1000x697 .jpg │ ├── G0MQW-3 2021.05.31.19.22.00 1000x747 .jpg │ ├── G0MQW-4 2021.05.31.19.22.00 1000x747 .jpg │ ├── G0UPL 2021.05.31.19.22.00 1000x692 .jpg │ ├── G3VYZ-40M-SLOW 2021.05.31.19.02.00 988x400 .jpg │ ├── G3VYZ-60M-SLOW 2021.05.31.19.02.00 975x334 .jpg │ ├── G3VYZ-FAST-10M 2021.05.31.19.22.00 927x379 .jpg │ ├── G3VYZ-FAST-40M 2021.05.31.19.22.00 914x440 .jpg │ ├── G3VYZ-FAST-60M 2021.05.31.19.22.00 825x347 .jpg │ ├── G4IOG-10M 2021.05.31.19.22.00 1452x1273 .png │ ├── G4IOG-20M 2021.05.31.19.11.59 1122x692 .png │ ├── G4IOG-30M 2021.05.31.19.22.00 1122x692 .png │ ├── G4IOG-40M 2021.05.31.19.22.00 1452x814 .png │ ├── G4IOG-80M 2021.05.31.19.22.00 1122x692 .png │ ├── K4RCG-1 2021.05.31.18.22.00 1000x597 .jpg │ ├── K4RCG-3 2021.05.31.18.12.00 1113x926 .jpg │ ├── K5MO-1 2021.05.31.19.22.00 969x1457 .jpg │ ├── KL7L-0 2021.05.31.19.22.00 1130x630 .jpg │ ├── KL7L-1 2021.05.31.19.22.00 1218x686 .jpg │ ├── KL7L-2 2021.05.31.19.22.00 1218x686 .jpg │ ├── LA5GOA-30M 2021.05.31.19.22.00 1202x702 .jpg │ ├── M0HGU 2021.05.31.19.22.00 1122x692 .jpg │ ├── N9JL-1 2021.05.31.19.22.00 1330x846 .jpg │ ├── OK1FCX-3-80M-NVIS 2021.05.31.19.02.00 1309x347 .jpg │ ├── PA2OHH-10M 2021.05.31.19.11.59 1122x692 .jpg │ ├── PA2OHH-30M 2021.05.31.19.22.00 1122x692 .jpg │ ├── PA2OHH-40M 2021.05.31.19.22.00 1122x692 .jpg │ ├── PA2OHH-80M 2021.05.31.19.11.59 1122x692 .jpg │ ├── SA6BSS-1 2021.05.31.19.22.00 1789x991 .jpg │ ├── SA6BSS-2 2021.05.31.19.22.00 1789x991 .jpg │ ├── SA6BSS-3 2021.05.31.19.22.00 1789x991 .jpg │ ├── TF3HZ-1 2021.05.31.19.22.00 1026x475 .jpg │ ├── VA3ROM-30M 2021.05.31.19.22.00 1549x975 .jpg │ ├── W1BW-10M 2021.05.31.19.22.00 1600x1000 .jpg │ ├── W1BW-12M 2021.05.31.19.22.00 1600x1000 .jpg │ ├── W1BW-17M 2021.05.31.19.22.00 1600x1000 .jpg │ ├── W1BW-30M 2021.05.31.19.22.00 1600x1000 .jpg │ ├── W6REK-20M 2021.05.31.19.22.00 1096x820 .png │ ├── W6REK-30M 2021.05.31.19.22.00 1096x820 .png │ ├── W6REK-40M 2021.05.31.19.22.00 1096x820 .png │ ├── W6REK-80J 2021.05.31.19.22.00 1096x820 .png │ ├── WA5DJJ-10M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-12M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-15M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-160M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-17M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-20M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-22M-HIFER1 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-22M-HIFER2 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-30M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-40M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-6M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-80M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WA5DJJ-WIDE-10M 2021.05.31.19.22.00 1122x692 .jpg │ ├── WD4AH 2021.05.31.19.22.00 1337x740 .jpg │ ├── WD4ELG-10-FAST 2021.05.31.19.22.00 1789x997 .jpg │ ├── WD4ELG-10-SLOW 2021.05.31.19.02.00 1789x997 .jpg │ ├── WD4ELG-20-FAST 2021.05.31.19.22.00 1789x997 .jpg │ ├── WD4ELG-20-SLOW 2021.05.31.19.02.00 1789x997 .jpg │ ├── WD4ELG-30-FAST 2021.05.31.19.22.00 1789x997 .jpg │ ├── WD4ELG-30-SLOW 2021.05.31.19.02.00 1789x997 .jpg │ ├── WD4ELG-40-FAST 2021.05.31.19.22.00 1789x997 .jpg │ ├── WD4ELG-40-SLOW 2021.05.31.19.02.00 1789x997 .jpg │ ├── WD4ELG-80-FAST 2021.05.31.14.52.00 1789x997 .jpg │ └── WD4ELG-80-SLOW 2021.05.31.14.02.00 1789x997 .jpg ├── grabbers.csv └── src ├── backend ├── QrssPlus.sln ├── QrssPlus │ ├── Grabber.cs │ ├── GrabberData.cs │ ├── GrabberHistory.cs │ ├── GrabberIO.cs │ ├── GrabberInfo.cs │ ├── ImageProcessing.cs │ └── QrssPlus.csproj ├── QrssPlusFunctions │ ├── .gitignore │ ├── Properties │ │ ├── PublishProfiles │ │ │ └── QrssPlusFunctions20210427223607 - Zip Deploy.pubxml │ │ ├── ServiceDependencies │ │ │ ├── QrssPlusFunctions20210427223607 - Zip Deploy │ │ │ │ ├── profile.arm.json │ │ │ │ └── storage1.arm.json │ │ │ └── local │ │ │ │ └── storage1.arm.json │ │ ├── serviceDependencies.QrssPlusFunctions20210427223607 - Zip Deploy.json │ │ ├── serviceDependencies.json │ │ └── serviceDependencies.local.json │ ├── QrssPlusFunctions.csproj │ ├── QrssPlusUpdate.cs │ └── host.json └── QrssPlusTests │ ├── CsvTests.cs │ ├── DownloadTests.cs │ ├── ImageTests.cs │ ├── JsonTests.cs │ ├── QrssPlusTests.csproj │ ├── SampleData.cs │ └── data │ ├── grabberStatus.json │ ├── grabbers.csv │ └── grabs │ ├── G0MQW-4 2021.04.28.21.22.00.jpg │ ├── K5MO-1 2021.04.28.15.42.00.jpg │ ├── KL7L-0 2021.04.28.16.32.00.jpg │ ├── PA2OHH-40M 2021.04.28.19.22.00.jpg │ ├── W6REK-40M 2021.04.28.15.42.00.png │ ├── demo.bmp │ └── demo.gif └── frontend ├── .gitignore ├── README.md ├── package.json ├── public ├── banner.jpg ├── favicon.ico └── index.html ├── src ├── App.js ├── App.test.js ├── components │ ├── Banner.js │ ├── Config.js │ ├── Dashboard.js │ ├── GrabberDetails.js │ ├── GrabberList.js │ ├── MobileView.js │ ├── News.js │ └── Thumbnails.js ├── index.js ├── reportWebVitals.js └── setupTests.js └── yarn.lock /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | name: Backend CI/CD 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | backend: 8 | name: Backend 9 | runs-on: windows-latest 10 | steps: 11 | - name: 🚚 Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: ✨ Use .NET Core 3.1 15 | uses: actions/setup-dotnet@v3 16 | with: 17 | dotnet-version: "3.1.x" 18 | 19 | - name: 🚚 Restore 20 | run: dotnet restore ./src/backend 21 | 22 | - name: 🛠️ Build 23 | run: dotnet build ./src/backend 24 | 25 | - name: 🧪 Test 26 | run: dotnet test ./src/backend 27 | 28 | - name: 🛠️ Build Function 29 | working-directory: ./src/backend/QrssPlusFunctions/ 30 | run: dotnet build --configuration Release --no-restore --output ./output 31 | 32 | - name: 🚀 Publish Function 33 | uses: Azure/functions-action@v1 34 | with: 35 | app-name: "QrssPlusUpdate" 36 | package: "./src/backend/QrssPlusFunctions/output" 37 | publish-profile: ${{ secrets.PUBLISH_PROFILE }} 38 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: Frontend CI/CD 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | frontend: 8 | name: Build and Deploy 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 🚚 Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: ⚙️ Setup Node 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: "16" 18 | cache: "yarn" 19 | cache-dependency-path: ./src/frontend/yarn.lock 20 | 21 | - name: 📦 Install packages 22 | working-directory: ./src/frontend 23 | run: yarn install 24 | 25 | - name: 🧪 Test 26 | working-directory: ./src/frontend 27 | run: yarn test 28 | 29 | - name: 🛠️ Build 30 | working-directory: ./src/frontend 31 | run: yarn build -------------------------------------------------------------------------------- /.github/workflows/grabbers.yml: -------------------------------------------------------------------------------- 1 | name: Validate Grabbers List 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - "grabbers.csv" 8 | 9 | jobs: 10 | backend: 11 | name: Run Tests 12 | runs-on: windows-latest 13 | steps: 14 | - name: 🚚 Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: ✨ Use .NET Core 3.1 18 | uses: actions/setup-dotnet@v1 19 | with: 20 | dotnet-version: "3.1.x" 21 | 22 | - name: 🚚 Restore 23 | run: dotnet restore ./src/backend 24 | 25 | - name: 🛠️ Build 26 | run: dotnet build ./src/backend 27 | 28 | - name: 🧪 Test 29 | run: dotnet test ./src/backend 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | .vs 4 | .env -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "razor.disableBlazorDebugPrompt": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Scott W Harden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # QRSS Plus 3 | 4 | [![Backend CI/CD](https://github.com/swharden/QRSSplus/actions/workflows/backend.yml/badge.svg)](https://github.com/swharden/QRSSplus/actions/workflows/backend.yml) 5 | [![Frontend CI/CD](https://github.com/swharden/QRSSplus/actions/workflows/frontend.yml/badge.svg)](https://github.com/swharden/QRSSplus/actions/workflows/frontend.yml) 6 | [![Validate Grabbers List](https://github.com/swharden/QRSSplus/actions/workflows/grabbers.yml/badge.svg)](https://github.com/swharden/QRSSplus/actions/workflows/grabbers.yml) 7 | 8 | **[QRSS Plus](https://www.swharden.com/qrss/plus) is an automatically-updating website that lists active QRSS grabbers around the world.** Every 10 minutes grabber URLs are read from [grabbers.csv](grabbers.csv), the latest grabs are downloaded and analyzed, and only grabbers whose spectrogram images changed recently are marked as "active" on the website. 9 | 10 | * **Launch QRSS Plus:** **https://www.swharden.com/qrss/plus** 11 | 12 | ## QRSS Grabber List 13 | 14 | **This list of grabbers is actively maintained by Andy (G0FTD)** and serves as the primary source QRSS Plus uses to get information about grabbers. It is downloaded automatically every 10 minutes. 15 | 16 | * [**grabbers.csv**](grabbers.csv) ([raw](https://raw.githubusercontent.com/swharden/QRSSplus/master/grabbers.csv)) 17 | 18 | ## Request a Change 19 | 20 | **To submit or modify a grabber listing,** E-mail Andy (G0FTD) punkbiscuit@googlemail.com or post a message to the [Knights QRSS Mailing List](https://groups.io/g/qrssknights) and provide the latest grabber information: 21 | 22 | * Callsign 23 | * Location 24 | * URL to the grabber image 25 | * URL to a personal website (optional) 26 | 27 | ⚠️ **Please do not submit non-functional URLs!** Test URLs on your own computer to ensure they function as expected before submitting them. 28 | 29 | ⚠️ **Dropbox users** must ensure their URL returns an _image file_, not a _web page_ that displays an image. To fix this, replace `www.dropbox.com` with `dl.dropboxusercontent.com` as shown here: 30 | 31 | 32 | ``` 33 | Bad URL: https://www.dropbox.com/s/35m4m8wn4w5hi7r/HF.jpg 34 | Good URL: http://dl.dropboxusercontent.com/s/35m4m8wn4w5hi7r/HF.jpg 35 | ``` 36 | 37 | If the URL contains a `?`, delete it and all characters following it: 38 | 39 | ``` 40 | Bad URL: https://www.dropbox.com/s/35m4m8wn4w5hi7r/HF.jpg?dl=0 41 | Good URL: https://www.dropbox.com/s/35m4m8wn4w5hi7r/HF.jpg 42 | ``` 43 | 44 | ## Additional Resources 45 | * [Knights QRSS Mailing List](https://groups.io/g/qrssknights) - The QRSS Knights is an extremely active community of QRSS enthusiasts. Join their mailing list to get in on discussions about new grabbers, radio spots, equipment, reception techniques, cosmic and atmospheric anomalies, and efforts in the QRSS world. 46 | * [74! 47 | The Knights QRSS Winter Newsletter](https://swharden.com/qrss/74) - Updated every December by Andy (G0FTD) 48 | * [The New Age of QRSS](https://swharden.com/blog/2020-10-03-new-age-of-qrss/) - A modern introduction to QRSS 49 | * [Knights QRSS Blog](http://knightsqrss.blogspot.com/) - Updated less frequently, but worth noting 50 | * [Getting Started with QRSS](http://knightsqrss.blogspot.com/2010/01/getting-started-with-qrss.html) - A good guide for those new to QRSS 51 | * [Carpe QRSS](https://github.com/strickyak/carpe-qrss) - A QRSS grabber built on the GO language 52 | * [LOPORA](http://www.qsl.net/pa2ohh/11lop.htm) - QRSS reception program in python 53 | * [QRP-Labs](https://www.qrp-labs.com/) - QRSS transmitter kits by [Hans Summers](http://www.hanssummers.com) 54 | * [QrssPiG](https://gitlab.com/hb9fxx/qrsspig) - QRSS Grabber for Raspberry Pi 55 | -------------------------------------------------------------------------------- /dev/QrssAnalysis/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Drawing; 5 | using System.Drawing.Drawing2D; 6 | using System.Drawing.Imaging; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Runtime.InteropServices; 10 | 11 | namespace QrssAnalysis 12 | { 13 | class Program 14 | { 15 | static void Main(string[] args) 16 | { 17 | var imagePaths = Directory.GetFiles("../../../../sample-images/").Select(x => Path.GetFullPath(x)).Skip(20); 18 | 19 | int count = 0; 20 | foreach (string imagePath in imagePaths) 21 | { 22 | using Bitmap bmp = new(imagePath); 23 | 24 | double[,] data2d = ReadBitmap2D(bmp); 25 | double[] data1d = FunctionByColumn(data2d, ArrayMean); 26 | 27 | var plt = new ScottPlot.Plot(); 28 | plt.Title(Path.GetFileName(imagePath)); 29 | plt.AddSignal(data1d); 30 | var hm = plt.AddHeatmapCoordinated(data2d, 0, data2d.GetLength(1), 1000, 2000, ScottPlot.Drawing.Colormap.Grayscale); 31 | string filename = Path.GetFullPath($"test-{count:000}.bmp"); 32 | plt.SaveFig(filename); 33 | 34 | Console.WriteLine(filename); 35 | break; 36 | } 37 | } 38 | 39 | public static double ArrayStdev(double[] input) 40 | { 41 | if (input is null) 42 | throw new ArgumentNullException(nameof(input)); 43 | 44 | if (input.Length == 0) 45 | throw new ArgumentException("must not be empty"); 46 | 47 | double sum = input.Sum(); 48 | double mean = sum / input.Length; 49 | double sumVariancesSquared = 0; 50 | for (int i = 0; i < input.Length; i++) 51 | { 52 | double pointVariance = Math.Abs(mean - input[i]); 53 | double pointVarianceSquared = Math.Pow(pointVariance, 2); 54 | sumVariancesSquared += pointVarianceSquared; 55 | } 56 | double meanVarianceSquared = sumVariancesSquared / input.Length; 57 | double stdev = Math.Sqrt(meanVarianceSquared); 58 | 59 | return stdev; 60 | } 61 | 62 | public static double ArrayMean(double[] input) 63 | { 64 | if (input is null) 65 | throw new ArgumentNullException(nameof(input)); 66 | 67 | if (input.Length == 0) 68 | throw new ArgumentException("must not be empty"); 69 | 70 | return input.Sum() / input.Length; 71 | } 72 | 73 | public static double ArrayMax(double[] input) 74 | { 75 | if (input is null) 76 | throw new ArgumentNullException(nameof(input)); 77 | 78 | if (input.Length == 0) 79 | throw new ArgumentException("must not be empty"); 80 | 81 | return input.Max(); 82 | } 83 | 84 | public static double ArrayMin(double[] input) 85 | { 86 | if (input is null) 87 | throw new ArgumentNullException(nameof(input)); 88 | 89 | if (input.Length == 0) 90 | throw new ArgumentException("must not be empty"); 91 | 92 | return input.Min(); 93 | } 94 | 95 | /// 96 | /// Collapse a 2D array into a 1D array by applying a function to each column of values 97 | /// 98 | /// 2d input data 99 | /// function to convert a column of values into a single value 100 | public static double[] FunctionByColumn(double[,] data, Func function) 101 | { 102 | int height = data.GetLength(0); 103 | int width = data.GetLength(1); 104 | 105 | double[] valsByColumn = new double[width]; 106 | for (int columnIndex = 0; columnIndex < width; columnIndex++) 107 | { 108 | double[] columnValues = Enumerable.Range(0, height).Select(rowIndex => data[rowIndex, columnIndex]).ToArray(); 109 | valsByColumn[columnIndex] = function(columnValues); 110 | } 111 | return valsByColumn; 112 | } 113 | 114 | /// 115 | /// Slice a 2D array into a smaller one containing the defined rows 116 | /// 117 | public static double[,] GetRows(double[,] input, int rowCount, int rowOffset) 118 | { 119 | double[,] output = new double[rowCount, input.GetLength(1)]; 120 | 121 | for (int i = 0; i < rowCount; i++) 122 | { 123 | for (int j = 0; j < input.GetLength(1); j++) 124 | { 125 | output[i, j] = input[i + rowOffset, j]; 126 | } 127 | } 128 | return output; 129 | } 130 | 131 | public static double[,] ReadBitmap2D(Bitmap bmp) 132 | { 133 | // lock the image and copy all its bytes 134 | Rectangle rect = new(0, 0, bmp.Width, bmp.Height); 135 | BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat); 136 | int byteCount = Math.Abs(bmpData.Stride) * bmp.Height; 137 | byte[] bytes = new byte[byteCount]; 138 | Marshal.Copy(bmpData.Scan0, bytes, 0, byteCount); 139 | bmp.UnlockBits(bmpData); 140 | 141 | // copy data from bytes into 2D array 142 | int bytesPerPixel = Image.GetPixelFormatSize(bmp.PixelFormat) / 8; 143 | int bytesPerRow = bmpData.Stride; 144 | double[,] data = new double[bmp.Height, bmp.Width]; 145 | for (int y = 0; y < bmp.Height; y++) 146 | { 147 | int rowOffset = bytesPerRow * y; 148 | for (int x = 0; x < bmp.Width; x++) 149 | { 150 | int pos = rowOffset + x * bytesPerPixel; 151 | byte r = bytes[pos]; 152 | byte g = bytes[pos + 1]; 153 | byte b = bytes[pos + 2]; 154 | //byte a = bytes[pos + 3]; 155 | data[y, x] = r + g + b; 156 | } 157 | } 158 | return data; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /dev/QrssAnalysis/QrssAnalysis.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dev/QrssAnalysis/QrssAnalysis.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31229.75 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QrssAnalysis", "QrssAnalysis.csproj", "{CF6FC0CF-9CD4-478F-B04E-8922D42FC545}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {CF6FC0CF-9CD4-478F-B04E-8922D42FC545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {CF6FC0CF-9CD4-478F-B04E-8922D42FC545}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {CF6FC0CF-9CD4-478F-B04E-8922D42FC545}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {CF6FC0CF-9CD4-478F-B04E-8922D42FC545}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {833539CD-E3AF-4FFE-B5DD-FB5DC9599924} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /dev/banner/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/banner/banner.jpg -------------------------------------------------------------------------------- /dev/banner/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/banner/banner.png -------------------------------------------------------------------------------- /dev/banner/banner.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/banner/banner.psd -------------------------------------------------------------------------------- /dev/execute.http: -------------------------------------------------------------------------------- 1 | # https://marketplace.visualstudio.com/items?itemName=humao.rest-client 2 | # variables defined in the `.env` file (same directory as this script) 3 | 4 | ### Execute locally 5 | POST http://localhost:7071/admin/functions/QrssPlusUpdate/ 6 | Content-Type: application/json 7 | 8 | {} 9 | 10 | ### Execute remotely in the cloud 11 | POST https://qrssplusfunctions20210427223607.azurewebsites.net/admin/functions/QrssPlusUpdate 12 | x-functions-key: {{$dotenv FUNCTIONSKEY}} 13 | Content-Type: application/json 14 | 15 | {} -------------------------------------------------------------------------------- /dev/icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/icon/icon.ico -------------------------------------------------------------------------------- /dev/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/icon/icon.png -------------------------------------------------------------------------------- /dev/icon/make_icon.bat: -------------------------------------------------------------------------------- 1 | convert icon.png icon.ico 2 | pause -------------------------------------------------------------------------------- /dev/sample-images/7L1RLL-136KHZ 2021.05.31.19.22.00 618x439 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/7L1RLL-136KHZ 2021.05.31.19.22.00 618x439 .jpg -------------------------------------------------------------------------------- /dev/sample-images/CT2IWW 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/CT2IWW 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/DL4DTL 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/DL4DTL 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/EA8BVP-30M-FAST 2021.05.31.19.22.00 1107x648 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/EA8BVP-30M-FAST 2021.05.31.19.22.00 1107x648 .jpg -------------------------------------------------------------------------------- /dev/sample-images/EA8BVP-30M-SLOW 2021.05.31.19.02.00 1104x647 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/EA8BVP-30M-SLOW 2021.05.31.19.02.00 1104x647 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G0MQW-1 2021.05.31.19.22.00 1000x747 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G0MQW-1 2021.05.31.19.22.00 1000x747 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G0MQW-2 2021.05.31.19.22.00 1000x697 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G0MQW-2 2021.05.31.19.22.00 1000x697 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G0MQW-3 2021.05.31.19.22.00 1000x747 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G0MQW-3 2021.05.31.19.22.00 1000x747 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G0MQW-4 2021.05.31.19.22.00 1000x747 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G0MQW-4 2021.05.31.19.22.00 1000x747 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G0UPL 2021.05.31.19.22.00 1000x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G0UPL 2021.05.31.19.22.00 1000x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G3VYZ-40M-SLOW 2021.05.31.19.02.00 988x400 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G3VYZ-40M-SLOW 2021.05.31.19.02.00 988x400 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G3VYZ-60M-SLOW 2021.05.31.19.02.00 975x334 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G3VYZ-60M-SLOW 2021.05.31.19.02.00 975x334 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G3VYZ-FAST-10M 2021.05.31.19.22.00 927x379 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G3VYZ-FAST-10M 2021.05.31.19.22.00 927x379 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G3VYZ-FAST-40M 2021.05.31.19.22.00 914x440 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G3VYZ-FAST-40M 2021.05.31.19.22.00 914x440 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G3VYZ-FAST-60M 2021.05.31.19.22.00 825x347 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G3VYZ-FAST-60M 2021.05.31.19.22.00 825x347 .jpg -------------------------------------------------------------------------------- /dev/sample-images/G4IOG-10M 2021.05.31.19.22.00 1452x1273 .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G4IOG-10M 2021.05.31.19.22.00 1452x1273 .png -------------------------------------------------------------------------------- /dev/sample-images/G4IOG-20M 2021.05.31.19.11.59 1122x692 .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G4IOG-20M 2021.05.31.19.11.59 1122x692 .png -------------------------------------------------------------------------------- /dev/sample-images/G4IOG-30M 2021.05.31.19.22.00 1122x692 .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G4IOG-30M 2021.05.31.19.22.00 1122x692 .png -------------------------------------------------------------------------------- /dev/sample-images/G4IOG-40M 2021.05.31.19.22.00 1452x814 .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G4IOG-40M 2021.05.31.19.22.00 1452x814 .png -------------------------------------------------------------------------------- /dev/sample-images/G4IOG-80M 2021.05.31.19.22.00 1122x692 .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/G4IOG-80M 2021.05.31.19.22.00 1122x692 .png -------------------------------------------------------------------------------- /dev/sample-images/K4RCG-1 2021.05.31.18.22.00 1000x597 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/K4RCG-1 2021.05.31.18.22.00 1000x597 .jpg -------------------------------------------------------------------------------- /dev/sample-images/K4RCG-3 2021.05.31.18.12.00 1113x926 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/K4RCG-3 2021.05.31.18.12.00 1113x926 .jpg -------------------------------------------------------------------------------- /dev/sample-images/K5MO-1 2021.05.31.19.22.00 969x1457 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/K5MO-1 2021.05.31.19.22.00 969x1457 .jpg -------------------------------------------------------------------------------- /dev/sample-images/KL7L-0 2021.05.31.19.22.00 1130x630 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/KL7L-0 2021.05.31.19.22.00 1130x630 .jpg -------------------------------------------------------------------------------- /dev/sample-images/KL7L-1 2021.05.31.19.22.00 1218x686 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/KL7L-1 2021.05.31.19.22.00 1218x686 .jpg -------------------------------------------------------------------------------- /dev/sample-images/KL7L-2 2021.05.31.19.22.00 1218x686 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/KL7L-2 2021.05.31.19.22.00 1218x686 .jpg -------------------------------------------------------------------------------- /dev/sample-images/LA5GOA-30M 2021.05.31.19.22.00 1202x702 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/LA5GOA-30M 2021.05.31.19.22.00 1202x702 .jpg -------------------------------------------------------------------------------- /dev/sample-images/M0HGU 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/M0HGU 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/N9JL-1 2021.05.31.19.22.00 1330x846 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/N9JL-1 2021.05.31.19.22.00 1330x846 .jpg -------------------------------------------------------------------------------- /dev/sample-images/OK1FCX-3-80M-NVIS 2021.05.31.19.02.00 1309x347 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/OK1FCX-3-80M-NVIS 2021.05.31.19.02.00 1309x347 .jpg -------------------------------------------------------------------------------- /dev/sample-images/PA2OHH-10M 2021.05.31.19.11.59 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/PA2OHH-10M 2021.05.31.19.11.59 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/PA2OHH-30M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/PA2OHH-30M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/PA2OHH-40M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/PA2OHH-40M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/PA2OHH-80M 2021.05.31.19.11.59 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/PA2OHH-80M 2021.05.31.19.11.59 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/SA6BSS-1 2021.05.31.19.22.00 1789x991 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/SA6BSS-1 2021.05.31.19.22.00 1789x991 .jpg -------------------------------------------------------------------------------- /dev/sample-images/SA6BSS-2 2021.05.31.19.22.00 1789x991 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/SA6BSS-2 2021.05.31.19.22.00 1789x991 .jpg -------------------------------------------------------------------------------- /dev/sample-images/SA6BSS-3 2021.05.31.19.22.00 1789x991 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/SA6BSS-3 2021.05.31.19.22.00 1789x991 .jpg -------------------------------------------------------------------------------- /dev/sample-images/TF3HZ-1 2021.05.31.19.22.00 1026x475 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/TF3HZ-1 2021.05.31.19.22.00 1026x475 .jpg -------------------------------------------------------------------------------- /dev/sample-images/VA3ROM-30M 2021.05.31.19.22.00 1549x975 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/VA3ROM-30M 2021.05.31.19.22.00 1549x975 .jpg -------------------------------------------------------------------------------- /dev/sample-images/W1BW-10M 2021.05.31.19.22.00 1600x1000 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/W1BW-10M 2021.05.31.19.22.00 1600x1000 .jpg -------------------------------------------------------------------------------- /dev/sample-images/W1BW-12M 2021.05.31.19.22.00 1600x1000 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/W1BW-12M 2021.05.31.19.22.00 1600x1000 .jpg -------------------------------------------------------------------------------- /dev/sample-images/W1BW-17M 2021.05.31.19.22.00 1600x1000 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/W1BW-17M 2021.05.31.19.22.00 1600x1000 .jpg -------------------------------------------------------------------------------- /dev/sample-images/W1BW-30M 2021.05.31.19.22.00 1600x1000 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/W1BW-30M 2021.05.31.19.22.00 1600x1000 .jpg -------------------------------------------------------------------------------- /dev/sample-images/W6REK-20M 2021.05.31.19.22.00 1096x820 .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/W6REK-20M 2021.05.31.19.22.00 1096x820 .png -------------------------------------------------------------------------------- /dev/sample-images/W6REK-30M 2021.05.31.19.22.00 1096x820 .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/W6REK-30M 2021.05.31.19.22.00 1096x820 .png -------------------------------------------------------------------------------- /dev/sample-images/W6REK-40M 2021.05.31.19.22.00 1096x820 .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/W6REK-40M 2021.05.31.19.22.00 1096x820 .png -------------------------------------------------------------------------------- /dev/sample-images/W6REK-80J 2021.05.31.19.22.00 1096x820 .png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/W6REK-80J 2021.05.31.19.22.00 1096x820 .png -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-10M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-10M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-12M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-12M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-15M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-15M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-160M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-160M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-17M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-17M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-20M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-20M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-22M-HIFER1 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-22M-HIFER1 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-22M-HIFER2 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-22M-HIFER2 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-30M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-30M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-40M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-40M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-6M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-6M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-80M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-80M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WA5DJJ-WIDE-10M 2021.05.31.19.22.00 1122x692 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WA5DJJ-WIDE-10M 2021.05.31.19.22.00 1122x692 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4AH 2021.05.31.19.22.00 1337x740 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4AH 2021.05.31.19.22.00 1337x740 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-10-FAST 2021.05.31.19.22.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-10-FAST 2021.05.31.19.22.00 1789x997 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-10-SLOW 2021.05.31.19.02.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-10-SLOW 2021.05.31.19.02.00 1789x997 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-20-FAST 2021.05.31.19.22.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-20-FAST 2021.05.31.19.22.00 1789x997 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-20-SLOW 2021.05.31.19.02.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-20-SLOW 2021.05.31.19.02.00 1789x997 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-30-FAST 2021.05.31.19.22.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-30-FAST 2021.05.31.19.22.00 1789x997 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-30-SLOW 2021.05.31.19.02.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-30-SLOW 2021.05.31.19.02.00 1789x997 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-40-FAST 2021.05.31.19.22.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-40-FAST 2021.05.31.19.22.00 1789x997 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-40-SLOW 2021.05.31.19.02.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-40-SLOW 2021.05.31.19.02.00 1789x997 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-80-FAST 2021.05.31.14.52.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-80-FAST 2021.05.31.14.52.00 1789x997 .jpg -------------------------------------------------------------------------------- /dev/sample-images/WD4ELG-80-SLOW 2021.05.31.14.02.00 1789x997 .jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/dev/sample-images/WD4ELG-80-SLOW 2021.05.31.14.02.00 1789x997 .jpg -------------------------------------------------------------------------------- /grabbers.csv: -------------------------------------------------------------------------------- 1 | #ID,callsign,title,name,location,website,file 2 | AA7US,AA7US,Variable band,John,QTH and state varies,http://www.qsl.net/aa7us/,http://www.qsl.net/aa7us/hf3.jpg 3 | # AJ4VD,AJ4VD,30m,Scott,"Gainesville, FL, USA",https://swharden.com/software/Fhttps://github.com/swharden/QRSSplus/edit/master/grabbers.csvSKview/,https://swharden.com/dev/grabber/latest.png 4 | EA8BVP-30m-Fast,EA8BVP,30m,Baltasar,Canary Islands,http://www.qsl.net/ea8bvp/grabber.html,http://www.qsl.net/ea8bvp/hf1.jpg 5 | EA8BVP-30m-Slow,EA8BVP,30m (4hr),Baltasar,Canary Islands,http://www.qsl.net/ea8bvp/grabber.html,http://www.qsl.net/ea8bvp/hf2.jpg 6 | G0FTD-NOMADIC,G0FTD,Tests,Andy,Kent,http://www.qsl.net/g0ftd/grabber.htm,http://www.qsl.net/g0ftd/hf1.jpg 7 | G0MQW-10m-Eu,G0MQW,EU QRSS band,Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,http://www.qsl.net/g0mqw/hf1.jpg 8 | G0MQW-10m-NA,G0MQW,North America QRSS band),Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,http://www.qsl.net/g0mqw/hf2.jpg 9 | G0MQW-6m,G0MQW,6m band,Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,http://www.qsl.net/g0mqw/hf3.jpg 10 | G0MQW-30m,G0MQW,30m,Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,http://www.qsl.net/g0mqw/hf4.jpg 11 | G0MQW-E,G0MQW,Variable,Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,https://www.qsl.net/g0mqw/hf5.jpg 12 | G0MQW-X,G0MQW,Experiments,Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,http://www.qsl.net/g0mqw/vhf5.jpg 13 | G3VYZ-Fast-10m,G3VYZ,Grabber 1,Les,"Holywell, Northumberland, UK",https://www.holywell44.com/qrss/qrss.htm,https://www.holywell44.com/qrss/qrss_.jpg 14 | G3VYZ-Fast-40m,G3VYZ,Grabber 3,Les,"Holywell, Northumberland, UK",https://www.holywell44.com/qrss/qrss.htm,https://www.holywell44.com/qrss/qrss_3.jpg 15 | G3VYZ-Fast-60m,G3VYZ,Grabber 2,Les,"Holywell, Northumberland, UK",https://www.holywell44.com/qrss/qrss.htm,https://www.holywell44.com/qrss/qrss_2.jpg 16 | G3VYZ-40m-Slow,G3VYZ,Grabber 4,Les,"Holywell, Northumberland, UK",https://www.holywell44.com/qrss/qrss.htm,https://www.holywell44.com/qrss/qrss_4.jpg 17 | G3VYZ-5,G3VYZ,Grabber 5,Les,"Holywell, Northumberland, UK",https://www.holywell44.com/qrss/qrss.htm,https://www.holywell44.com/qrss/qrss_5.jpg 18 | G3VYZ-60m-Slow,G3VYZ,Grabber 6,Les,"Holywell, Northumberland, UK",http://www.holywell44.com/qrss/qrss.htm,http://www.holywell44.com/qrss/qrss_6.jpg 19 | G4IOG-6m,G4IOG,50MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,http://qsl.net/g4iog/6mdir/hf6.png 20 | G4IOG-10m,G4IOG,28MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,http://qsl.net/g4iog/10mdir/hf1.png 21 | G4IOG-12m,G4IOG,24.9MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,https://qsl.net/g4iog/12mdir/hf12.png 22 | G4IOG-15m,G4IOG,21MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,http://qsl.net/g4iog/15mdir/hf15.png 23 | G4IOG-17m,G4IOG,18.1MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,https://qsl.net/g4iog/17mdir/hf17.png 24 | G4IOG-20m,G4IOG,14MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,https://www.qsl.net/g4iog/20mdir/hf2.png 25 | G4IOG-30m,G4IOG,10MHz,Bob,"Newington, Kent, England",https://www.qsl.net/g4iog/30mdir/30mpage.html,https://www.qsl.net/g4iog/30mdir/hf3.png 26 | G4IOG-40m,G4IOG,7MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,https://www.qsl.net/g4iog/40mdir/hf4.png 27 | G4IOG-60m,G4IOG,5.3MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,https://www.qsl.net/g4iog/60mdir/hf60.png 28 | G4IOG-80m,G4IOG,3.5MHz,Bob,"Newington, Kent, England",https://www.qsl.net/g4iog/80mdir/80mpage.html,https://www.qsl.net/g4iog/80mdir/hf80.png 29 | G4IOG-160m,G4IOG,1.8MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,https://qsl.net/g4iog/160mdir/hf160.png 30 | G6AVK,G6AVK,Various bands,Colin,"Essex, England",http://www.qsl.net/g6avk/,http://www.qsl.net/g6avk/hf6.jpg 31 | HB9FXX-10m,HB9FXX,28MHz,Martin,Poland,https://hb9fxx.ch/qrss/,https://hb9fxx.ch/qrss/uploads/hf1.png 32 | JA1NGA-20m,JA1NGA,Experimenting,Kenji,Japan,https://qsl.net/ja1nga/hf1.jpg,https://qsl.net/ja1nga/hf1.jpg 33 | K4BYN-1,K4BYN,EXPERIMENTAL,Louis,North Carolina,https://www.qsl.net/k4byn/hf1,https://www.qsl.net/k4byn/hf1.jpg 34 | K4BYN-2,K4BYN,EXPERIMENTAL,Louis,North Carolina,https://www.qsl.net/k4byn/hf2,https://www.qsl.net/k4byn/hf2.jpg 35 | K5MO-Fast-10m,K5MO,10m band,John,North Carolina,https://qsl.net/k5mo/,https://qsl.net/k5mo/hf3.jpg 36 | K5MO-10m-Long,K5MO,10m band,John,North Carolina,https://qsl.net/k5mo/,https://qsl.net/k5mo/hf4.jpg 37 | K5MO-22m,K5MO,22M band,John,North Carolina,https://qsl.net/k5mo/,https://www.qsl.net/k5mo/hifer.jpg 38 | K5MO-Long-22m,K5MO,22M band,John,North Carolina,https://qsl.net/k5mo/,https://qsl.net/k5mo/longhifer.jpg 39 | KO4BHX-10m,KO4BHX,10m band,Ben,North Carolina,https://qsl.net/ko4bhx/,https://qsl.net/ko4bhx/qrss/10m.jpg 40 | KL7L-0,KL7L,Band can vary,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/10Alaska00000.jpg 41 | KL7L-1,KL7L,Band can vary,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/15Alaska00000.jpg 42 | KL7L-2,KL7L,Band can vary,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/17Alaska00000.jpg 43 | KL7L-3,KL7L,Band can vary,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/20Alaska00000.jpg 44 | KL7L-4,KL7L,Band can vary,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/20aAlaska00000.jpg 45 | KL7L-5,KL7L,Band can vary,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/22Alaska00000.jpg 46 | KL7L-6,KL7L,Band can vary,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/30aAlaska00000.jpg 47 | KL7L-7,KL7L,Band can vary,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/40Alaska00000.jpg 48 | KL7L-8,KL7L,Band can vary,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/60Alaska00000.jpg 49 | KL7L-XX,KL7L,Special ops,Laurence,"Wasilla,Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/XXAlaska00000.jpg 50 | KQ4LMV-10m,KQ4LMV,EXPERIMENTAL,Scott,North Carolina,https://qsl.net/kq4lmv/,https://qsl.net/kq4lmv/QRSS/kq4lmv-10m-qrss.png 51 | KQ4LMV-20m,KQ4LMV,EXPERIMENTAL,Scott,North Carolina,https://qsl.net/kq4lmv/,https://qsl.net/kq4lmv/QRSS/kq4lmv-20m-qrss.png 52 | KQ4LMV-40m,KQ4LMV,EXPERIMENTAL,Scott,North Carolina,https://qsl.net/kq4lmv/,https://qsl.net/kq4lmv/QRSS/kq4lmv-40m-qrss.png 53 | LA5GOA-30m,LA5GOA,30m,Steen,Norway,http://la5goa.manglet.net/grabber,https://qsl.net/la5goa/hf1.jpg 54 | M0BMN,M0BMN,Various,Paul,Wolverhampton,http://www.phoenixkitsonline.co.uk,http://www.phoenixkitsonline.co.uk/qrss.jpg 55 | M0HGU-30m,M0HGU,Usually 30m,Nick,Norfolk UK,https://www.dctower.co.uk/qrss/,https://www.dctower.co.uk/qrss/grab.jpg 56 | M0GBZ,M0GBZ,Various part time,Euan,Southern UK,https://qsl.net/m0gbz/,https://qsl.net/m0gbz/hf1.jpg 57 | M0PWX-6m,M0PWX,6m part time,Pete,Isle of Sheppey,https://qsl.net/m0pwx/grabbers.htm,https://qsl.net/m0pwx/grabs/6m/6mhf1.jpg 58 | M0PWX-10m,M0PWX,10m part time,Pete,Isle of Sheppey,https://qsl.net/m0pwx/grabbers.htm,https://qsl.net/m0pwx/grabs/10m/10mhf1.jpg 59 | M0PWX-15m,M0PWX,15m part time,Pete,Isle of Sheppey,https://qsl.net/m0pwx/grabbers.htm,https://www.qsl.net/m0pwx/grabs/15m/15mhf1.jpg 60 | M0PWX-17m,M0PWX,17m part time,Pete,Isle of Sheppey,https://qsl.net/m0pwx/grabbers.htm,https://qsl.net/m0pwx/grabs/17m/17mhf1.jpg 61 | M0PWX-Fast-20m,M0PWX,20m part time,Pete,Isle of Sheppey,https://qsl.net/m0pwx/grabbers.htm,https://www.qsl.net/m0pwx/grabs/20m/20mhf1.jpg 62 | M0PWX-22m,M0PWX,22m part time,Pete,Isle of Sheppey,https://qsl.net/m0pwx/grabbers.htm,https://www.qsl.net/m0pwx/grabs/22m/22mhf1.jpg 63 | M0PWX-30m,M0PWX,30m part time,Pete,Isle of Sheppey,https://qsl.net/m0pwx/grabbers.htm,https://www.qsl.net/m0pwx/grabs/30m/30mhf1.jpg 64 | M0PWX-40m,M0PWX,40m part time,Pete,Isle of Sheppey,https://qsl.net/m0pwx/grabbers.htm,https://qsl.net/m0pwx/grabs/40m/40mhf1.jpg 65 | M0PWX-60m,M0PWX,60m part time,Pete,Isle of Sheppey,https://qsl.net/m0pwx/grabbers.htm,https://qsl.net/m0pwx/grabs/60m/60mhf1.jpg 66 | M0PWX-80m,M0PWX,80m part time,Peter,Isle of Sheppey,https://www.qsl.net/m/m0pwx/,https://www.qsl.net/m/m0pwx/grabs/80m/80mhf1.jpg 67 | # M0RON,M0RON,Testing,Andy,Bishops Cleeve,https://www.qsl.net/m0ron/,https://www.qsl.net/m0ron/grabs/hf1.png 68 | N0LUF-1,N0LUF,Testing,Bryan,Colorado US,https://www.qsl.net/n0luf,https://www.qsl.net/n0luf/hf1.jpg 69 | N0LUF-2,N0LUF,Testing,Bryan,Colorado US,https://www.qsl.net/n0luf,https://www.qsl.net/n0luf/hf2.jpg 70 | N0LUF-3,N0LUF,Testing,Bryan,Colorado US,https://www.qsl.net/n0luf,https://www.qsl.net/n0luf/hf3.jpg 71 | N0LUF-4,N0LUF,Testing,Bryan,Colorado US,https://www.qsl.net/n0luf,https://www.qsl.net/n0luf/hf4.jpg 72 | N9JL-1,N9JL,30m FSKVIEW,John,Shorewood IL,https://www.qsl.net/n9jl,https://www.qsl.net/n9jl/capt.jpg 73 | N8NJ-1,N8NJ,Various bands,Larry,Ohio,https://www.qsl.net/n8nj/index.html,https://www.qsl.net/n8nj/hf1.jpg 74 | N8NJ-2,N8NJ,Various bands,Larry,Ohio,https://www.qsl.net/n8nj/index.html,https://www.qsl.net/n8nj/hf2.jpg 75 | OK1FCX-1,OK1FCX,Experimental,Radovan,"Czech Republic",http://www.qsl.net/ok1fcx/,http://www.qsl.net/ok1fcx/hf1.jpg 76 | OK1FCX-2,OK1FCX,Experimental,Radovan,"Czech Republic",http://www.qsl.net/ok1fcx/,http://www.qsl.net/ok1fcx/hf2.jpg 77 | OK1FCX-3,OK1FCX,Experimental,Radovan,"Czech Republic",http://www.qsl.net/ok1fcx/,http://www.qsl.net/ok1fcx/hf3.jpg 78 | OK1FCX-6m,OK1FCX,Experimental,Radovan,"Czech Republic",http://www.qsl.net/ok1fcx/,http://www.qsl.net/ok1fcx/hf4.jpg 79 | ON4CDJ,ON4CDJ,Various,Patrick,Belgium,http://qsl.net/on4cdj/qrss,http://qsl.net/on4cdj/qrss/hf1.jpg 80 | PA2OHH-10m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf5.jpg 81 | PA2OHH-12-15m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf6.jpg 82 | PA2OHH-20m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf4.jpg 83 | PA2OHH-30m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf1.jpg 84 | PA2OHH-40m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf2.jpg 85 | PA2OHH-60m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf8.jpg 86 | PA2OHH-80m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf3.jpg 87 | S52AS-30m,S52AS,30m Part time,Roman,"Novo Mesto, Slovenia",http://novomesto.zevs.si/graber.htm,http://novomesto.zevs.si/podatki/test.gif 88 | SA6BSS-10m,SA6BSS,10m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf4.jpg 89 | SA6BSS-12m,SA6BSS,12m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/mf2.jpg 90 | SA6BSS-15m,SA6BSS,15m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf6.jpg 91 | SA6BSS-17m,SA6BSS,17m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf7.jpg 92 | SA6BSS-20m,SA6BSS,20m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf3.jpg 93 | SA6BSS-30m,SA6BSS,30m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf1.jpg 94 | SA6BSS-40m,SA6BSS,40m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf2.jpg 95 | SA6BSS-60m,SA6BSS,60m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf9.jpg 96 | SA6BSS-80m,SA6BSS,80m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf5.jpg 97 | SA6BSS-160m,SA6BSS,160m,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/mf1.jpg 98 | SA6BSS-X,SA6BSS,Random tests,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf8.jpg 99 | SO5CW-30m,DJ5CW,Experimental,Fabian,Warsaw - Poland KO01MW,https://fkurz.net/ham/qrss/,https://fkurz.net/ham/qrss/30m.png 100 | SO5CW-LONG-30m,DJ5CW,Experimental,Fabian,Warsaw - Poland KO01MW,https://fkurz.net/ham/qrss/,https://fkurz.net/ham/qrss/fabian-30m-24h-view.png 101 | SQ4MIK-1,SQ4MIK,Variable,Piotr,Poland,https://www.qsl.net/sq4mik,https://www.qsl.net/sq4mik/hf1.jpg 102 | TF3HZ-1,TF3HZ,Band varies,Haldor,Iceland,http://halldorg.is/,https://www.qsl.net/tf3hz/tf3hzgrabb.jpg 103 | VA3VF-1,VA3VF,Testing,Vince,Canada,https://www.qsl.net/va3vf/,https://www.qsl.net/va3vf/latest.jpg 104 | VA3ROM-22m,VA3ROM,30m,Robert,"Ontario, Canada",http://va3rom.com/QRSS/QRSS.html,http://va3rom.com/QRSS/image1.jpg 105 | VA3RYV,VA3RYV,MISC BANDS,Wes,"Ontario,Canada",https://qsl.net/va3ryv/,http://www.qsl.net/va3ryv/grabber/hf1.jpg 106 | VE1VDM-1,VE1VDM,Various,Vernon,"Nova Scotia, Canada",https://www.qsl.net/ve1vdm/,https://www.qsl.net/ve1vdm/argo.jpg 107 | VE1VDM-2,VE1VDM,Various,Vernon,"Nova Scotia, Canada",https://www.qsl.net/ve1vdm/,https://www.qsl.net/ve1vdm/argo1.jpg 108 | VK3EDW,VK3EDW,Primary,John,Kallista - Victoria,http://www.qsl.net/vk3edw,http://www.qsl.net/vk3edw/QRSS/assets/images/hf1.jpg 109 | VK4AAN-10m,VK4AAN,10m,Alan,Queensland,https://www.qsl.net/v/vk4aan/,https://www.qsl.net/v/vk4aan/lopora/lst20m10.jpg 110 | VK4AAN-24hr-10m,VK4AAN,10m,Alan,Queensland,https://www.qsl.net/v/vk4aan/,https://www.qsl.net/v//vk4aan/lopora/lst24h10.jpg 111 | VK4AAN-30m,VK4AAN,30m,Alan,Queensland,https://www.qsl.net/v/vk4aan/,https://www.qsl.net/v/vk4aan/lopora/lst20m30.jpg 112 | VK4AAN-24hr-30m,VK4AAN,30m,Alan,Queensland,https://www.qsl.net/v/vk4aan/,https://www.qsl.net/v//vk4aan/lopora/lst24h30.jpg 113 | VK4BAP-10m,VK4BAP,Testing,Brian,Queensland Australia,https://www.qsl.net/vk4bap/,https://www.qsl.net/vk4bap/10m/hf1.png 114 | VK4BAP-17m,VK4BAP,Testing,Brian,Queensland Australia,https://www.qsl.net/vk4bap/,https://www.qsl.net/vk4bap/17m/hf4.png 115 | VK4BAP-20m,VK4BAP,Testing,Brian,Queensland Australia,https://www.qsl.net/vk4bap/,https://www.qsl.net/vk4bap/20m/hf3.png 116 | VK4BAP-30m,VK4BAP,Testing,Brian,Queensland Australia,https://www.qsl.net/vk4bap/,https://www.qsl.net/vk4bap/30m/hf5.png 117 | VK4BAP-40m,VK4BAP,Testing,Brian,Queensland Australia,https://www.qsl.net/vk4bap/,https://www.qsl.net/vk4bap/40m/hf2.png 118 | VE7IGH-30m,VE7IGH,10Mhz & attic antenna,Gregory,"British Columbia,Canada",http://www.qsl.net/ve7igh/Grabber.htm,http://www.qsl.net/ve7igh/capt.jpg 119 | W1BW-WIDESHORT,W1BW,0-38MHZ WIDEVIEW SHORT,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/0/latest.jpg 120 | W1BW-WIDELONG,W1BW,0-38MHZ WIDEVIEW LONG,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/0/slow.jpg 121 | W1BW-10m,W1BW,10m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/28/latest.jpg 122 | W1BW-12m,W1BW,12m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/24/latest.jpg 123 | W1BW-15m,W1BW,15m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us/,https://grabber.w1bw.us/21/latest.jpg 124 | W1BW-17m,W1BW,17m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/18/latest.jpg 125 | W1BW-20m,W1BW,20m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/14/latest.jpg 126 | W1BW-22m,W1BW,22m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/13/latest.jpg 127 | W1BW-30m,W1BW,30m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/10/latest.jpg 128 | W1BW-40m,W1BW,40m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/7/latest.jpg 129 | W1BW-60m,W1BW,60m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/5/latest.jpg 130 | W1BW-80m,W1BW,80m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/3/latest.jpg 131 | W1BW-160m,W1BW,160m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/1/latest.jpg 132 | W1BW-28-SLOW,W1BW,10m,Bruce,"QTH Islesboro, Maine",https://grabber.w1bw.us,https://grabber.w1bw.us/28/slow.jpg 133 | WD4AH,WD4AH,Testing,Alfred,Florida,http://www.qsl.net/wd4ah,http://www.qsl.net/wd4ah/hf1.jpg 134 | WD4ELG-10-Fast,WD4ELG,10m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_10M_grabber_fast.jpg 135 | WD4ELG-15-Fast,WD4ELG,15m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_15M_grabber_fast.jpg 136 | WD4ELG-20-Fast,WD4ELG,20m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_20M_grabber_fast.jpg 137 | WD4ELG-30-Fast,WD4ELG,30m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_30M_grabber_fast.jpg 138 | WD4ELG-40-Fast,WD4ELG,40m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_40M_grabber_fast.jpg 139 | WD4ELG-80-Fast,WD4ELG,80m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_80M_grabber_fast.jpg 140 | WD4ELG-160-Fast,WD4ELG,160m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_160M_grabber_fast.jpg 141 | WD4ELG-10-Slow,WD4ELG,10m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_10M_grabber_slow.jpg 142 | WD4ELG-15-Slow,WD4ELG,15m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_15M_grabber_slow.jpg 143 | WD4ELG-20-Slow,WD4ELG,20m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_20M_grabber_slow.jpg 144 | WD4ELG-30-Slow,WD4ELG,30m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_30M_grabber_slow.jpg 145 | WD4ELG-40-Slow,WD4ELG,40m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_40M_grabber_slow.jpg 146 | WD4ELG-80-Slow,WD4ELG,80m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_80M_grabber_slow.jpg 147 | WD4ELG-160-Slow,WD4ELG,80m grabber,Mark,"Greensboro, NC",https://qsl.net/wd4elg/,https://qsl.net/wd4elg/WD4ELG_160M_grabber_slow.jpg 148 | WM9C-1,WM9C,New May 2025,Matt,"Illinois",https://wm9c.telegraphy.de,https://wm9c.telegraphy.de/WM9C30m.png 149 | W6REK-20m,W6REK,20m,Henry,"San Jose, California",http://yak.net/qrss/,http://yak.net/qrss/W6REK-20m.png 150 | W6REK-30m,W6REK,30m,Henry,"San Jose, California",http://yak.net/qrss/,http://yak.net/qrss/W6REK-30m.png 151 | W6REK-40m,W6REK,40m,Henry,"San Jose, California",http://yak.net/qrss/,http://yak.net/qrss/W6REK-40m.png 152 | W6REK-80j,W6REK,80j,Henry,"San Jose, California",http://yak.net/qrss/,http://yak.net/qrss/W6REK-80j.png 153 | WA5DJJ-6m,WA5DJJ,6m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,https://www.qsl.net/wa5djj/mf5.jpg 154 | WA5DJJ-10m,WA5DJJ,10m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf8.jpg 155 | WA5DJJ-Wide-10m,WA5DJJ,10m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,https://qsl.net/wa5djj/uhf7.jpg 156 | WA5DJJ-12m,WA5DJJ,12m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf7.jpg 157 | WA5DJJ-15m,WA5DJJ,15m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf6.jpg 158 | WA5DJJ-17m,WA5DJJ,17m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf5.jpg 159 | WA5DJJ-20m,WA5DJJ,20m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf4.jpg 160 | WA5DJJ-22m-Hifer1,WA5DJJ,22m HiFer,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,https://qsl.net/wa5djj/uhf4.jpg 161 | WA5DJJ-22m-Hifer2,WA5DJJ,22m HiFer,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,https://qsl.net/wa5djj/vlf1.jpg 162 | WA5DJJ-30m,WA5DJJ,30m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf3.jpg 163 | WA5DJJ-40m,WA5DJJ,40m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf2.jpg 164 | WA5DJJ-80m,WA5DJJ,80m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/mf3.jpg 165 | WA5DJJ-160m,WA5DJJ,160m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/mf2.jpg 166 | -------------------------------------------------------------------------------- /src/backend/QrssPlus.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31205.134 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QrssPlusTests", "QrssPlusTests\QrssPlusTests.csproj", "{513B91B0-3D86-4D8C-A573-9852C6E7A022}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QrssPlus", "QrssPlus\QrssPlus.csproj", "{81458CF2-0405-4970-9D3E-A47A726D606B}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QrssPlusFunctions", "QrssPlusFunctions\QrssPlusFunctions.csproj", "{B04C24A8-14D9-40D2-B10E-89DEFA4F81EF}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {513B91B0-3D86-4D8C-A573-9852C6E7A022}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {513B91B0-3D86-4D8C-A573-9852C6E7A022}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {513B91B0-3D86-4D8C-A573-9852C6E7A022}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {513B91B0-3D86-4D8C-A573-9852C6E7A022}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {81458CF2-0405-4970-9D3E-A47A726D606B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {81458CF2-0405-4970-9D3E-A47A726D606B}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {81458CF2-0405-4970-9D3E-A47A726D606B}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {81458CF2-0405-4970-9D3E-A47A726D606B}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {B04C24A8-14D9-40D2-B10E-89DEFA4F81EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {B04C24A8-14D9-40D2-B10E-89DEFA4F81EF}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {B04C24A8-14D9-40D2-B10E-89DEFA4F81EF}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {B04C24A8-14D9-40D2-B10E-89DEFA4F81EF}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {410E69F7-663C-44BC-9226-6AF5C1804095} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/backend/QrssPlus/Grabber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text; 7 | 8 | namespace QrssPlus 9 | { 10 | public class Grabber 11 | { 12 | public readonly GrabberInfo Info; 13 | public readonly GrabberHistory History; 14 | public readonly GrabberData Data; 15 | 16 | public Grabber(GrabberInfo info = null, GrabberHistory history = null) 17 | { 18 | Info = info ?? new GrabberInfo(); 19 | History = history ?? new GrabberHistory(); 20 | Data = new GrabberData(); 21 | } 22 | 23 | public override string ToString() 24 | { 25 | if (Data.Hash is null) 26 | return $"{Info.ID} error={Data.Response}"; 27 | else 28 | return $"{Info.ID} hash={Data.Hash}"; 29 | } 30 | 31 | public void DownloadLatestGrab(DateTime dt) 32 | { 33 | Data.Download(Info, dt); 34 | Data.ContainsNewUniqueImage = Data.Hash != null && Data.Hash != History.LastUniqueHash; 35 | if (Data.ContainsNewUniqueImage) 36 | { 37 | History.LastUniqueHash = Data.Hash; 38 | History.LastUniqueDateTime = dt; 39 | } 40 | History.LastUniqueAgeMinutes = (int)(dt - History.LastUniqueDateTime).TotalMinutes; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/backend/QrssPlus/GrabberData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Text; 8 | 9 | namespace QrssPlus 10 | { 11 | public class GrabberData 12 | { 13 | public byte[] Bytes { get; private set; } 14 | public string Hash { get; private set; } 15 | public DateTime DateTime { get; private set; } 16 | public string Response { get; private set; } 17 | public string Filename { get; private set; } 18 | public int ImageWidth { get; private set; } = 0; 19 | public int ImageHeight { get; private set; } = 0; 20 | public bool HasImageData => Bytes != null && Bytes.Length > 0; 21 | public bool ContainsNewUniqueImage = false; 22 | 23 | public void Download(GrabberInfo info, DateTime dt) 24 | { 25 | DateTime = dt; 26 | using WebClient client = new WebClient(); 27 | try 28 | { 29 | Bytes = client.DownloadData(info.ImageUrl); 30 | 31 | if (Bytes is null || Bytes.Length == 0) 32 | throw new WebException("Image URL contains no data"); 33 | 34 | if (Bytes[0] == '<') 35 | throw new WebException("Image URL contains HTML (not an image)"); 36 | 37 | using MemoryStream msIn = new MemoryStream(Bytes); 38 | Image originalImage; 39 | try { 40 | originalImage = Bitmap.FromStream(msIn); 41 | } catch (Exception e) { 42 | string errorMessage = $"image creation from downloaded bytes fail {info.ImageUrl}"; 43 | //throw new InvalidOperationException(errorMessage); 44 | Response = "fail"; 45 | return; 46 | } 47 | ImageWidth = originalImage.Width; 48 | ImageHeight = originalImage.Height; 49 | 50 | string timestamp = $"{dt.Year:D2}.{dt.Month:D2}.{dt.Day:D2}.{dt.Hour:D2}.{dt.Minute:D2}.{dt.Second:D2}"; 51 | Filename = $"{info.ID} {timestamp} {ImageWidth}x{ImageHeight} " + Path.GetExtension(info.ImageUrl); 52 | 53 | Hash = GetHash(Bytes); 54 | Response = "success"; 55 | } 56 | catch (WebException ex) 57 | { 58 | Response = ex.Message; 59 | } 60 | } 61 | 62 | private static string GetHash(byte[] data) 63 | { 64 | var md5 = System.Security.Cryptography.MD5.Create(); 65 | byte[] hashBytes = md5.ComputeHash(data); 66 | string hashString = string.Join("", hashBytes.Select(x => x.ToString("x2"))); 67 | return hashString; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/backend/QrssPlus/GrabberHistory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace QrssPlus 6 | { 7 | public class GrabberHistory 8 | { 9 | public string LastUniqueHash; 10 | public DateTime LastUniqueDateTime; 11 | public int LastUniqueAgeMinutes = -1; 12 | 13 | public string[] URLs = new string[] { }; 14 | 15 | public void Update(GrabberHistory oldHistory) 16 | { 17 | LastUniqueHash = oldHistory.LastUniqueHash; 18 | LastUniqueDateTime = oldHistory.LastUniqueDateTime; 19 | LastUniqueAgeMinutes = oldHistory.LastUniqueAgeMinutes; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/backend/QrssPlus/GrabberIO.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.RegularExpressions; 9 | using System.Threading.Tasks; 10 | 11 | namespace QrssPlus 12 | { 13 | public static class GrabberIO 14 | { 15 | /// 16 | /// Return an array of grabbers from a local CSV file 17 | /// 18 | public static Grabber[] GrabbersFromCsvFile(string filePath) 19 | { 20 | filePath = Path.GetFullPath(filePath); 21 | if (!File.Exists(filePath)) 22 | throw new InvalidOperationException($"CSV not exist: {filePath}"); 23 | string text = File.ReadAllText(filePath); 24 | return GrabbersFromCsvText(text); 25 | } 26 | 27 | /// 28 | /// Return an array of grabbers from a CSV file on the internet 29 | /// 30 | public static async Task GrabbersFromCsvUrl(string url) 31 | { 32 | var client = new HttpClient(); 33 | var response = await client.GetAsync(url); 34 | string text = await response.Content.ReadAsStringAsync(); 35 | Grabber[] grabbers = GrabbersFromCsvText(text); 36 | return grabbers; 37 | } 38 | 39 | public static Grabber[] GrabbersFromCsvText(string csv) => 40 | csv 41 | .Split("\n") 42 | .Select(line => GrabberFromCsvLine(line)) 43 | .Where(x => x != null) 44 | .ToArray(); 45 | 46 | /// 47 | /// Try to parse a CSV line and return a grabber (or null if it cannot be parsed) 48 | /// 49 | private static Grabber GrabberFromCsvLine(string line) 50 | { 51 | line = line.Trim(); 52 | 53 | if (line.StartsWith("#")) 54 | return null; 55 | 56 | string[] parts = new Regex(",(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))").Split(line); 57 | parts = parts.Select(s => s.Trim(new char[] { '\'', '"', ' ' })).ToArray(); 58 | if (parts.Length != 7) 59 | return null; 60 | 61 | var info = new GrabberInfo() 62 | { 63 | ID = parts[0], 64 | Callsign = parts[1], 65 | Title = parts[2], 66 | Name = parts[3], 67 | Location = parts[4], 68 | SiteUrl = parts[5], 69 | ImageUrl = parts[6], 70 | }; 71 | return new Grabber(info); 72 | } 73 | 74 | /// 75 | /// Create a JSON status file describing a list of grabbers 76 | /// 77 | public static string GrabbersToJson(Grabber[] grabbers) 78 | { 79 | const string API_VERSION = "1.0"; 80 | 81 | DateTime dt = DateTime.UtcNow; 82 | 83 | using var stream = new MemoryStream(); 84 | var options = new JsonWriterOptions() { Indented = true }; 85 | using var writer = new Utf8JsonWriter(stream, options); 86 | 87 | writer.WriteStartObject(); 88 | writer.WriteString("version", API_VERSION); 89 | writer.WriteString("created", dt); 90 | writer.WriteStartObject("grabbers"); 91 | foreach (var grabber in grabbers) 92 | { 93 | writer.WriteStartObject(grabber.Info.ID); 94 | 95 | // info 96 | writer.WriteString("id", grabber.Info.ID); 97 | writer.WriteString("name", grabber.Info.Name); 98 | writer.WriteString("callsign", grabber.Info.Callsign); 99 | writer.WriteString("location", grabber.Info.Location); 100 | writer.WriteString("imageUrl", grabber.Info.ImageUrl); 101 | writer.WriteString("siteUrl", grabber.Info.SiteUrl); 102 | 103 | // history 104 | writer.WriteString("lastResponse", grabber.Data.Response); 105 | writer.WriteString("lastUniqueHash", grabber.History.LastUniqueHash); 106 | writer.WriteString("lastUniqueDateTime", grabber.History.LastUniqueDateTime); 107 | writer.WriteNumber("lastUniqueAgeMinutes", grabber.History.LastUniqueAgeMinutes); 108 | writer.WriteNumber("lastUniqueAgeDays", grabber.History.LastUniqueAgeMinutes / (60 * 24)); 109 | 110 | // images 111 | writer.WriteStartArray("urls"); 112 | foreach (string url in grabber.History.URLs) 113 | writer.WriteStringValue(url); 114 | writer.WriteEndArray(); 115 | 116 | writer.WriteEndObject(); 117 | } 118 | writer.WriteEndObject(); 119 | writer.WriteEndObject(); 120 | 121 | writer.Flush(); 122 | string json = Encoding.UTF8.GetString(stream.ToArray()); 123 | 124 | return json; 125 | } 126 | 127 | public static Grabber[] GrabbersFromJson(string json) 128 | { 129 | const string EXPECTED_VERSION = "1.0"; 130 | 131 | using JsonDocument document = JsonDocument.Parse(json); 132 | 133 | string version = document.RootElement.GetProperty("version").GetString(); 134 | if (version != EXPECTED_VERSION) 135 | throw new InvalidOperationException("invalid JSON version"); 136 | 137 | List grabbers = new List(); 138 | foreach (var grabber in document.RootElement.GetProperty("grabbers").EnumerateObject()) 139 | { 140 | GrabberInfo info = new GrabberInfo() 141 | { 142 | ID = grabber.Value.GetProperty("id").GetString(), 143 | Name = grabber.Value.GetProperty("name").GetString(), 144 | Callsign = grabber.Value.GetProperty("callsign").GetString(), 145 | Location = grabber.Value.GetProperty("location").GetString(), 146 | ImageUrl = grabber.Value.GetProperty("imageUrl").GetString(), 147 | SiteUrl = grabber.Value.GetProperty("siteUrl").GetString(), 148 | }; 149 | 150 | GrabberHistory history = new GrabberHistory() 151 | { 152 | LastUniqueHash = grabber.Value.GetProperty("lastUniqueHash").GetString(), 153 | LastUniqueDateTime = grabber.Value.GetProperty("lastUniqueDateTime").GetDateTime(), 154 | LastUniqueAgeMinutes = grabber.Value.GetProperty("lastUniqueAgeMinutes").GetInt32(), 155 | URLs = grabber.Value.GetProperty("urls").EnumerateArray().Select(x => x.GetString()).ToArray() 156 | }; 157 | 158 | grabbers.Add(new Grabber(info, history)); 159 | } 160 | 161 | return grabbers.ToArray(); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/backend/QrssPlus/GrabberInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace QrssPlus 7 | { 8 | public class GrabberInfo 9 | { 10 | private string _ID; 11 | public string ID { get => _ID; set { _ID = SanitizeID(value); } } 12 | public string Callsign; 13 | public string Title; 14 | public string Name; 15 | public string Location; 16 | public string ImageUrl; 17 | public string SiteUrl; 18 | 19 | private static string SanitizeID(string id) 20 | { 21 | if (id is null) 22 | throw new ArgumentException("ID cannot be null"); 23 | 24 | var validChars = id.ToUpper().ToCharArray().Where(c => char.IsLetterOrDigit(c) || c == '-'); 25 | 26 | if (validChars.Count() == 0) 27 | throw new ArgumentException("ID contains no valid characters"); 28 | 29 | return string.Join("", validChars); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/backend/QrssPlus/ImageProcessing.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Drawing.Drawing2D; 5 | using System.Drawing.Imaging; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace QrssPlus 11 | { 12 | public static class ImageProcessing 13 | { 14 | public static byte[] GetThumbnailSkinny(byte[] bytes) => GetThumbnailBytes(bytes, quality: 50, height: 500, width: 25); 15 | 16 | public static byte[] GetThumbnailAuto(byte[] bytes) => GetThumbnailBytes(bytes, quality: 85, height: 100, width: 150); 17 | 18 | public static byte[] GetThumbnailBytes(byte[] bytes, int quality, int height, int width = -1) 19 | { 20 | using MemoryStream msIn = new MemoryStream(bytes); 21 | Image originalImage = Bitmap.FromStream(msIn); 22 | 23 | if (width <= 0) 24 | width = (int)(height * ((double)originalImage.Width / originalImage.Height)); 25 | 26 | Image thumbnailImage = Resize(originalImage, width, height); 27 | 28 | using MemoryStream msOut = new MemoryStream(); 29 | 30 | EncoderParameters encoderParams; 31 | try { 32 | encoderParams = new EncoderParameters(1); 33 | } catch (Exception e) { 34 | throw new InvalidOperationException("EncoderParameters construction fail", e); 35 | } 36 | 37 | try { 38 | encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality); 39 | } catch (Exception e) { 40 | throw new InvalidOperationException("EncoderParameter construction fail", e); 41 | } 42 | 43 | ImageCodecInfo myImageCodecInfo; 44 | try { 45 | myImageCodecInfo = ImageCodecInfo.GetImageEncoders().Where(x => x.MimeType == "image/jpeg").First(); 46 | } catch (Exception e) { 47 | throw new InvalidOperationException("ImageCodecInfo fail", e); 48 | } 49 | 50 | try { 51 | thumbnailImage.Save(msOut, myImageCodecInfo, encoderParams); 52 | } catch (Exception e) { 53 | throw new InvalidOperationException("thumbnail save fail", e); 54 | } 55 | 56 | return msOut.ToArray(); 57 | } 58 | 59 | private static Image Resize(Image image, int width, int height) 60 | { 61 | var destRect = new Rectangle(0, 0, width, height); 62 | var destImage = new Bitmap(width, height); 63 | 64 | destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); 65 | 66 | using var graphics = Graphics.FromImage(destImage); 67 | graphics.CompositingMode = CompositingMode.SourceCopy; 68 | graphics.CompositingQuality = CompositingQuality.HighQuality; 69 | graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; 70 | graphics.SmoothingMode = SmoothingMode.HighQuality; 71 | graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; 72 | 73 | using var wrapMode = new ImageAttributes(); 74 | wrapMode.SetWrapMode(WrapMode.TileFlipXY); 75 | graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode); 76 | 77 | return destImage; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/backend/QrssPlus/QrssPlus.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/Properties/PublishProfiles/QrssPlusFunctions20210427223607 - Zip Deploy.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | ZipDeploy 8 | AzureWebSite 9 | Release 10 | Any CPU 11 | https://qrssplusfunctions20210427223607.azurewebsites.net 12 | False 13 | /subscriptions/c7f65b75-f0ee-44b7-a52c-0cca4ef05c67/resourcegroups/QrssPlus/providers/Microsoft.Web/sites/QrssPlusFunctions20210427223607 14 | $QrssPlusFunctions20210427223607 15 | <_SavePWD>True 16 | https://qrssplusfunctions20210427223607.scm.azurewebsites.net/ 17 | 18 | -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/Properties/ServiceDependencies/QrssPlusFunctions20210427223607 - Zip Deploy/profile.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_dependencyType": "function.windows.appService" 6 | }, 7 | "parameters": { 8 | "resourceGroupName": { 9 | "type": "string", 10 | "defaultValue": "QrssPlus", 11 | "metadata": { 12 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 13 | } 14 | }, 15 | "resourceGroupLocation": { 16 | "type": "string", 17 | "defaultValue": "eastus2", 18 | "metadata": { 19 | "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." 20 | } 21 | }, 22 | "resourceName": { 23 | "type": "string", 24 | "defaultValue": "QrssPlusFunctions20210427223607", 25 | "metadata": { 26 | "description": "Name of the main resource to be created by this template." 27 | } 28 | }, 29 | "resourceLocation": { 30 | "type": "string", 31 | "defaultValue": "[parameters('resourceGroupLocation')]", 32 | "metadata": { 33 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 34 | } 35 | } 36 | }, 37 | "resources": [ 38 | { 39 | "type": "Microsoft.Resources/resourceGroups", 40 | "name": "[parameters('resourceGroupName')]", 41 | "location": "[parameters('resourceGroupLocation')]", 42 | "apiVersion": "2019-10-01" 43 | }, 44 | { 45 | "type": "Microsoft.Resources/deployments", 46 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 47 | "resourceGroup": "[parameters('resourceGroupName')]", 48 | "apiVersion": "2019-10-01", 49 | "dependsOn": [ 50 | "[parameters('resourceGroupName')]" 51 | ], 52 | "properties": { 53 | "mode": "Incremental", 54 | "expressionEvaluationOptions": { 55 | "scope": "inner" 56 | }, 57 | "parameters": { 58 | "resourceGroupName": { 59 | "value": "[parameters('resourceGroupName')]" 60 | }, 61 | "resourceGroupLocation": { 62 | "value": "[parameters('resourceGroupLocation')]" 63 | }, 64 | "resourceName": { 65 | "value": "[parameters('resourceName')]" 66 | }, 67 | "resourceLocation": { 68 | "value": "[parameters('resourceLocation')]" 69 | } 70 | }, 71 | "template": { 72 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 73 | "contentVersion": "1.0.0.0", 74 | "parameters": { 75 | "resourceGroupName": { 76 | "type": "string" 77 | }, 78 | "resourceGroupLocation": { 79 | "type": "string" 80 | }, 81 | "resourceName": { 82 | "type": "string" 83 | }, 84 | "resourceLocation": { 85 | "type": "string" 86 | } 87 | }, 88 | "variables": { 89 | "storage_name": "[toLower(concat('storage', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId))))]", 90 | "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", 91 | "storage_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Storage/storageAccounts/', variables('storage_name'))]", 92 | "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]", 93 | "function_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/sites/', parameters('resourceName'))]" 94 | }, 95 | "resources": [ 96 | { 97 | "location": "[parameters('resourceLocation')]", 98 | "name": "[parameters('resourceName')]", 99 | "type": "Microsoft.Web/sites", 100 | "apiVersion": "2015-08-01", 101 | "tags": { 102 | "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" 103 | }, 104 | "dependsOn": [ 105 | "[variables('appServicePlan_ResourceId')]", 106 | "[variables('storage_ResourceId')]" 107 | ], 108 | "kind": "functionapp", 109 | "properties": { 110 | "name": "[parameters('resourceName')]", 111 | "kind": "functionapp", 112 | "httpsOnly": true, 113 | "reserved": false, 114 | "serverFarmId": "[variables('appServicePlan_ResourceId')]", 115 | "siteConfig": { 116 | "alwaysOn": true 117 | } 118 | }, 119 | "identity": { 120 | "type": "SystemAssigned" 121 | }, 122 | "resources": [ 123 | { 124 | "name": "appsettings", 125 | "type": "config", 126 | "apiVersion": "2015-08-01", 127 | "dependsOn": [ 128 | "[variables('function_ResourceId')]" 129 | ], 130 | "properties": { 131 | "AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]", 132 | "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]", 133 | "FUNCTIONS_EXTENSION_VERSION": "~3", 134 | "FUNCTIONS_WORKER_RUNTIME": "dotnet" 135 | } 136 | } 137 | ] 138 | }, 139 | { 140 | "location": "[parameters('resourceGroupLocation')]", 141 | "name": "[variables('storage_name')]", 142 | "type": "Microsoft.Storage/storageAccounts", 143 | "apiVersion": "2017-10-01", 144 | "tags": { 145 | "[concat('hidden-related:', concat('/providers/Microsoft.Web/sites/', parameters('resourceName')))]": "empty" 146 | }, 147 | "properties": { 148 | "supportsHttpsTrafficOnly": true 149 | }, 150 | "sku": { 151 | "name": "Standard_LRS" 152 | }, 153 | "kind": "Storage" 154 | }, 155 | { 156 | "location": "[parameters('resourceGroupLocation')]", 157 | "name": "[variables('appServicePlan_name')]", 158 | "type": "Microsoft.Web/serverFarms", 159 | "apiVersion": "2015-08-01", 160 | "sku": { 161 | "name": "S1", 162 | "tier": "Standard", 163 | "family": "S", 164 | "size": "S1" 165 | }, 166 | "properties": { 167 | "name": "[variables('appServicePlan_name')]" 168 | } 169 | } 170 | ] 171 | } 172 | } 173 | } 174 | ] 175 | } -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/Properties/ServiceDependencies/QrssPlusFunctions20210427223607 - Zip Deploy/storage1.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceGroupName": { 6 | "type": "string", 7 | "defaultValue": "QrssPlus", 8 | "metadata": { 9 | "_parameterType": "resourceGroup", 10 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 11 | } 12 | }, 13 | "resourceGroupLocation": { 14 | "type": "string", 15 | "defaultValue": "eastus2", 16 | "metadata": { 17 | "_parameterType": "location", 18 | "description": "Location of the resource group. Resource groups could have different location than resources." 19 | } 20 | }, 21 | "resourceLocation": { 22 | "type": "string", 23 | "defaultValue": "[parameters('resourceGroupLocation')]", 24 | "metadata": { 25 | "_parameterType": "location", 26 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 27 | } 28 | } 29 | }, 30 | "resources": [ 31 | { 32 | "type": "Microsoft.Resources/resourceGroups", 33 | "name": "[parameters('resourceGroupName')]", 34 | "location": "[parameters('resourceGroupLocation')]", 35 | "apiVersion": "2019-10-01" 36 | }, 37 | { 38 | "type": "Microsoft.Resources/deployments", 39 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('qrssplus', subscription().subscriptionId)))]", 40 | "resourceGroup": "[parameters('resourceGroupName')]", 41 | "apiVersion": "2019-10-01", 42 | "dependsOn": [ 43 | "[parameters('resourceGroupName')]" 44 | ], 45 | "properties": { 46 | "mode": "Incremental", 47 | "template": { 48 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 49 | "contentVersion": "1.0.0.0", 50 | "resources": [ 51 | { 52 | "sku": { 53 | "name": "Standard_LRS", 54 | "tier": "Standard" 55 | }, 56 | "kind": "StorageV2", 57 | "name": "qrssplus", 58 | "type": "Microsoft.Storage/storageAccounts", 59 | "location": "[parameters('resourceLocation')]", 60 | "apiVersion": "2017-10-01" 61 | } 62 | ] 63 | } 64 | } 65 | } 66 | ], 67 | "metadata": { 68 | "_dependencyType": "storage.azure" 69 | } 70 | } -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/Properties/ServiceDependencies/local/storage1.arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceGroupName": { 6 | "type": "string", 7 | "defaultValue": "QrssPlus", 8 | "metadata": { 9 | "_parameterType": "resourceGroup", 10 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." 11 | } 12 | }, 13 | "resourceGroupLocation": { 14 | "type": "string", 15 | "defaultValue": "eastus2", 16 | "metadata": { 17 | "_parameterType": "location", 18 | "description": "Location of the resource group. Resource groups could have different location than resources." 19 | } 20 | }, 21 | "resourceLocation": { 22 | "type": "string", 23 | "defaultValue": "[parameters('resourceGroupLocation')]", 24 | "metadata": { 25 | "_parameterType": "location", 26 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." 27 | } 28 | } 29 | }, 30 | "resources": [ 31 | { 32 | "type": "Microsoft.Resources/resourceGroups", 33 | "name": "[parameters('resourceGroupName')]", 34 | "location": "[parameters('resourceGroupLocation')]", 35 | "apiVersion": "2019-10-01" 36 | }, 37 | { 38 | "type": "Microsoft.Resources/deployments", 39 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('qrssplus', subscription().subscriptionId)))]", 40 | "resourceGroup": "[parameters('resourceGroupName')]", 41 | "apiVersion": "2019-10-01", 42 | "dependsOn": [ 43 | "[parameters('resourceGroupName')]" 44 | ], 45 | "properties": { 46 | "mode": "Incremental", 47 | "template": { 48 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 49 | "contentVersion": "1.0.0.0", 50 | "resources": [ 51 | { 52 | "sku": { 53 | "name": "Standard_LRS", 54 | "tier": "Standard" 55 | }, 56 | "kind": "StorageV2", 57 | "name": "qrssplus", 58 | "type": "Microsoft.Storage/storageAccounts", 59 | "location": "[parameters('resourceLocation')]", 60 | "apiVersion": "2017-10-01" 61 | } 62 | ] 63 | } 64 | } 65 | } 66 | ], 67 | "metadata": { 68 | "_dependencyType": "storage.azure" 69 | } 70 | } -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/Properties/serviceDependencies.QrssPlusFunctions20210427223607 - Zip Deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "storage1": { 4 | "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.Storage/storageAccounts/qrssplus", 5 | "type": "storage.azure", 6 | "connectionId": "AzureWebJobsStorage" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "storage1": { 4 | "type": "storage", 5 | "connectionId": "AzureWebJobsStorage" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "storage1": { 4 | "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.Storage/storageAccounts/qrssplus", 5 | "type": "storage.azure", 6 | "connectionId": "AzureWebJobsStorage" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/QrssPlusFunctions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | PreserveNewest 19 | Never 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/QrssPlusUpdate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Azure.Storage.Blobs; 9 | using Azure.Storage.Blobs.Models; 10 | using Microsoft.Azure.WebJobs; 11 | using Microsoft.Azure.WebJobs.Host; 12 | using Microsoft.Extensions.Logging; 13 | using QrssPlus; 14 | 15 | namespace QrssPlusFunctions 16 | { 17 | public static class QrssPlusUpdate 18 | { 19 | private const string GRABBERS_JSON_FILENAME = "grabbers.json"; 20 | private const string GRAB_FOLDER_PATH = "grabs/"; 21 | private const string GRAB_FOLDER_URL = "https://qrssplus.z20.web.core.windows.net/grabs/"; 22 | 23 | [FunctionName("QrssPlusUpdate")] 24 | public static void Run([TimerTrigger("0 2,12,22,32,42,52 * * * *")] TimerInfo myTimer, ILogger log) 25 | { 26 | DateTime dt = DateTime.UtcNow; 27 | log.LogInformation($"Starting update at {dt}"); 28 | 29 | BlobContainerClient webBlobClient = new BlobContainerClient( 30 | connectionString: Environment.GetEnvironmentVariable("AzureWebJobsStorage", EnvironmentVariableTarget.Process), 31 | blobContainerName: "$web"); 32 | 33 | Grabber[] grabbers = GetGrabbers(log); 34 | UpdateGrabberHistory(grabbers, webBlobClient, log); 35 | 36 | Parallel.ForEach(grabbers, grabber => 37 | { 38 | grabber.DownloadLatestGrab(dt); 39 | if (grabber.Data.ContainsNewUniqueImage) 40 | StoreImageData(grabber, webBlobClient, log); 41 | }); 42 | DeleteOldGrabs(maxAge: TimeSpan.FromHours(8), webBlobClient, log); 43 | UpdateGrabberURLs(grabbers, webBlobClient, log); 44 | SaveStatusFile(grabbers, webBlobClient, log); 45 | } 46 | 47 | /// 48 | /// Read the grabber list to get the latest grabber information 49 | /// 50 | private static Grabber[] GetGrabbers(ILogger log, int maximumCount = 999) 51 | { 52 | string grabberCsvUrl = "https://raw.githubusercontent.com/swharden/QRSSplus/master/grabbers.csv"; 53 | int linuxTime = (int)DateTime.Now.Subtract(new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; 54 | grabberCsvUrl += $"?v={linuxTime}"; 55 | log.LogInformation($"getting list of grabbers from: {grabberCsvUrl}"); 56 | Grabber[] grabbers = GrabberIO.GrabbersFromCsvUrl(grabberCsvUrl).Result; 57 | return grabbers.Take(maximumCount).ToArray(); 58 | } 59 | 60 | /// 61 | /// Read the JSON status file to update the history of the given grabbers 62 | /// 63 | private static void UpdateGrabberHistory(Grabber[] grabbers, BlobContainerClient container, ILogger log) 64 | { 65 | log.LogInformation($"reading information from stored grabber file: {GRABBERS_JSON_FILENAME}"); 66 | BlobClient blob = container.GetBlobClient(GRABBERS_JSON_FILENAME); 67 | if (!blob.Exists()) 68 | return; 69 | 70 | using MemoryStream stream = new MemoryStream(); 71 | blob.DownloadTo(stream); 72 | string json = Encoding.UTF8.GetString(stream.ToArray()); 73 | Grabber[] oldGrabbers = GrabberIO.GrabbersFromJson(json); 74 | 75 | Dictionary oldGrabberDictionary = new Dictionary(); 76 | foreach (Grabber oldGrabber in oldGrabbers) 77 | { 78 | if (oldGrabberDictionary.ContainsKey(oldGrabber.Info.ID)) 79 | oldGrabberDictionary.Remove(oldGrabber.Info.ID); 80 | 81 | oldGrabberDictionary.Add(oldGrabber.Info.ID, oldGrabber); 82 | } 83 | 84 | foreach (Grabber grabber in grabbers.Where(x => oldGrabberDictionary.ContainsKey(x.Info.ID))) 85 | grabber.History.Update(oldGrabberDictionary[grabber.Info.ID].History); 86 | 87 | log.LogInformation($"read information about {grabbers.Length} grabbers"); 88 | } 89 | 90 | /// 91 | /// Save a grabber's image data as a new file in blob storage 92 | /// 93 | private static void StoreImageData(Grabber grabber, BlobContainerClient container, ILogger log) 94 | { 95 | log.LogInformation($"{grabber}: prepare blob header"); 96 | BlobHttpHeaders headers = new BlobHttpHeaders() { ContentType = "image/jpeg", ContentLanguage = "en-us", }; 97 | 98 | log.LogInformation($"{grabber}: set blob header"); 99 | BlobClient blobOriginal = container.GetBlobClient(Path.Combine(GRAB_FOLDER_PATH, grabber.Data.Filename)); 100 | using var streamOriginal = new MemoryStream(grabber.Data.Bytes); 101 | blobOriginal.Upload(streamOriginal, overwrite: true); 102 | blobOriginal.SetHttpHeaders(headers); 103 | 104 | log.LogInformation($"{grabber}: set skinny blob header"); 105 | BlobClient blobThumbSkinny = container.GetBlobClient(Path.Combine(GRAB_FOLDER_PATH, grabber.Data.Filename + "-thumb-skinny.jpg")); 106 | using var streamThumbSkinny = new MemoryStream(ImageProcessing.GetThumbnailSkinny(grabber.Data.Bytes)); 107 | blobThumbSkinny.Upload(streamThumbSkinny, overwrite: true); 108 | blobOriginal.SetHttpHeaders(headers); 109 | 110 | log.LogInformation($"{grabber}: set thumb blob header"); 111 | BlobClient blobThumbAuto = container.GetBlobClient(Path.Combine(GRAB_FOLDER_PATH, grabber.Data.Filename + "-thumb-auto.jpg")); 112 | using var streamThumbAuto = new MemoryStream(ImageProcessing.GetThumbnailAuto(grabber.Data.Bytes)); 113 | blobThumbAuto.Upload(streamThumbAuto, overwrite: true); 114 | blobOriginal.SetHttpHeaders(headers); 115 | } 116 | 117 | /// 118 | /// Delete blob files older than a given age 119 | /// 120 | private static void DeleteOldGrabs(TimeSpan maxAge, BlobContainerClient container, ILogger log) 121 | { 122 | log.LogInformation($"Identifying old grab images..."); 123 | 124 | string[] oldBlobNames = container.GetBlobs() 125 | .Where(x => (DateTime.UtcNow - x.Properties.LastModified) > maxAge) 126 | .Select(x => x.Name) 127 | .ToArray(); 128 | 129 | log.LogInformation($"Deleting {oldBlobNames.Length} old grab images..."); 130 | 131 | foreach (var bloboldBlobName in oldBlobNames) 132 | container.DeleteBlob(bloboldBlobName); 133 | } 134 | 135 | /// 136 | /// Update the grab URLs for each grabber with those currently in blob storage 137 | /// 138 | private static void UpdateGrabberURLs(Grabber[] grabbers, BlobContainerClient container, ILogger log) 139 | { 140 | log.LogInformation($"updating URLs for watched grabbers"); 141 | 142 | string[] allFilenames = container 143 | .GetBlobs() 144 | .Where(x => x.Name.StartsWith(GRAB_FOLDER_PATH)) 145 | .Select(x => Path.GetFileName(x.Name)) 146 | .ToArray(); 147 | 148 | foreach (Grabber grabber in grabbers) 149 | grabber.History.URLs = allFilenames 150 | .Where(x => x.StartsWith(grabber.Info.ID)) 151 | .Where(x => !x.Contains("-thumb-")) 152 | .Select(x => GRAB_FOLDER_URL + x) 153 | .ToArray(); 154 | } 155 | 156 | /// 157 | /// Create a JSON summary of all grabbers and save it as a flat file in blob storage 158 | /// 159 | private static void SaveStatusFile(Grabber[] grabbers, BlobContainerClient container, ILogger log) 160 | { 161 | log.LogInformation($"saving {GRABBERS_JSON_FILENAME} at UTC {DateTime.UtcNow}"); 162 | 163 | string json = GrabberIO.GrabbersToJson(grabbers); 164 | 165 | // show the first few lines in the debug log 166 | string[] jsonLines = json.Split("\n"); 167 | foreach (string line in jsonLines.Take(5)) 168 | { 169 | log.LogInformation(line); 170 | } 171 | 172 | byte[] jsonBytes = Encoding.UTF8.GetBytes(json); 173 | 174 | BlobClient blob = container.GetBlobClient(GRABBERS_JSON_FILENAME); 175 | using var stream = new MemoryStream(jsonBytes, writable: false); 176 | blob.Upload(stream, overwrite: true); 177 | 178 | log.LogInformation($"uploaded {GRABBERS_JSON_FILENAME}"); 179 | 180 | BlobHttpHeaders headers = new BlobHttpHeaders { ContentType = "text/plain", ContentLanguage = "en-us" }; 181 | blob.SetHttpHeaders(headers); 182 | log.LogInformation($"set headers {GRABBERS_JSON_FILENAME}"); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/backend/QrssPlusFunctions/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/CsvTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | 7 | namespace QrssPlusTests 8 | { 9 | internal class CsvTests 10 | { 11 | [Test] 12 | public void Test_VerifyCsv_NoDuplicateIDs() 13 | { 14 | string csvFilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "../../../../../../grabbers.csv"); 15 | csvFilePath = Path.GetFullPath(csvFilePath); 16 | if (!File.Exists(csvFilePath)) 17 | throw new FileNotFoundException(csvFilePath); 18 | 19 | string csv = File.ReadAllText(csvFilePath); 20 | 21 | QrssPlus.Grabber[] grabbers = QrssPlus.GrabberIO.GrabbersFromCsvText(csv); 22 | 23 | var ids = new HashSet(); 24 | foreach (var grabber in grabbers) 25 | { 26 | if (ids.Contains(grabber.Info.ID)) 27 | throw new InvalidOperationException($"Duplicate ID: {grabber.Info.ID}"); 28 | 29 | ids.Add(grabber.Info.ID); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/DownloadTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using QrssPlus; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace QrssPlusTests 10 | { 11 | [Ignore("Ignore HTTP tests")] 12 | class DownloadTests 13 | { 14 | [Test] 15 | public void Test_Download_ImageData() 16 | { 17 | Grabber[] grabbers = GrabberIO.GrabbersFromCsvFile(SampleData.GRABBERS_CSV_PATH); 18 | DateTime dt = DateTime.UtcNow; 19 | 20 | Parallel.ForEach(grabbers, grabber => 21 | { 22 | grabber.DownloadLatestGrab(dt); 23 | Console.WriteLine(grabber); 24 | }); 25 | 26 | string json = GrabberIO.GrabbersToJson(grabbers); 27 | File.WriteAllText("grabberStatus.json", json); 28 | } 29 | 30 | [Test] 31 | public void Test_Download_GrabberListCSV() 32 | { 33 | string url = "https://raw.githubusercontent.com/swharden/QRSSplus/master/grabbers.csv"; 34 | Grabber[] grabbers = GrabberIO.GrabbersFromCsvUrl(url).Result; 35 | Assert.Greater(grabbers.Length, 20); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/ImageTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.IO; 6 | using System.Text; 7 | 8 | namespace QrssPlusTests 9 | { 10 | class ImageTests 11 | { 12 | [Test] 13 | public void Test_Image_Thumbnails() 14 | { 15 | foreach (string imageFilePath in System.IO.Directory.GetFiles(SampleData.GRABER_IMAGES_PATH)) 16 | { 17 | byte[] bytesIn = File.ReadAllBytes(imageFilePath); 18 | 19 | File.WriteAllBytes( 20 | path: Path.GetFileName(imageFilePath) + " thumb-skinny.jpg", 21 | bytes: QrssPlus.ImageProcessing.GetThumbnailBytes(bytesIn, 50, 500, 50)); 22 | 23 | File.WriteAllBytes( 24 | path: Path.GetFileName(imageFilePath) + " thumb-auto.jpg", 25 | bytes: QrssPlus.ImageProcessing.GetThumbnailBytes(bytesIn, 50, 250)); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/JsonTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using QrssPlus; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Text.Json; 9 | 10 | namespace QrssPlusTests 11 | { 12 | class JsonTests 13 | { 14 | [Test] 15 | public void Test_Status_GrabbersToJson() 16 | { 17 | Grabber[] grabbers = GrabberIO.GrabbersFromCsvFile(SampleData.GRABBERS_CSV_PATH); 18 | string json = GrabberIO.GrabbersToJson(grabbers); 19 | Console.WriteLine(json); 20 | Assert.Greater(json.Length, 1000); 21 | } 22 | 23 | [Test] 24 | public void Test_Status_GrabbersFromJson() 25 | { 26 | string json = File.ReadAllText(SampleData.GRABBERSTATUS_JSON_PATH); 27 | Grabber[] grabbers = GrabberIO.GrabbersFromJson(json); 28 | Assert.AreEqual(SampleData.GRABBERSTATUS_JSON_COUNT, grabbers.Count()); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/QrssPlusTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/SampleData.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using QrssPlus; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace QrssPlusTests 10 | { 11 | public static class SampleData 12 | { 13 | public static string GRABER_IMAGES_PATH = Path.Combine(TestContext.CurrentContext.TestDirectory, "../../../data/grabs/"); 14 | public static string GRABBERS_CSV_PATH = Path.Combine(TestContext.CurrentContext.TestDirectory, "../../../data/grabbers.csv"); 15 | public static int GRABBERS_CSV_COUNT = 121; 16 | public static string GRABBERSTATUS_JSON_PATH = Path.Combine(TestContext.CurrentContext.TestDirectory, "../../../data/grabberStatus.json"); 17 | public static int GRABBERSTATUS_JSON_COUNT = 121; 18 | 19 | [Test] 20 | public static void Test_GrabberListCSV_CanBeParsed() 21 | { 22 | Grabber[] grabbers = GrabberIO.GrabbersFromCsvFile(GRABBERS_CSV_PATH); 23 | Assert.AreEqual(GRABBERS_CSV_COUNT, grabbers.Length); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/data/grabbers.csv: -------------------------------------------------------------------------------- 1 | #ID,callsign,title,name,location,website,file 2 | 7L1RLL-136Khz,7L1RLL,136Khz,Rick,Japan,http://www1.u-netsurf.ne.jp/~7l1rll/Grabber_7L1RLL.html,http://www1.u-netsurf.ne.jp/~7l1rll/argo_capture.jpg 3 | # AA7US,AA7US,Variable band,John,QTH and state varies,http://www.qsl.net/aa7us/,http://www.qsl.net/aa7us/hf3.jpg 4 | AJ4VD,AJ4VD,30m,Scott,"Gainesville, FL, USA",https://swharden.com/software/FSKview/,https://swharden.com/dev/grabber/latest.png 5 | CT2IWW,CT2IWW,Usually 10m seasonal,Paulo,Portugal,http://qsl.net/ct2iww/CT2IWW_QRSS.html,http://qsl.net/ct2iww/CT2IWW_QRSS_files/hf1.jpg 6 | DL4DTL,DL4DTL,40m,Maik,South Germany,http://qsl.net/dl4dtl/index.htm,http://qsl.net/dl4dtl/hf2.jpg 7 | EA8BVP-30m-Fast,EA8BVP,30m,Baltasar,Canary Islands,http://www.qsl.net/ea8bvp/grabber.html,http://www.qsl.net/ea8bvp/hf1.jpg 8 | EA8BVP-30m-Slow,EA8BVP,30m (4hr),Baltasar,Canary Islands,http://www.qsl.net/ea8bvp/grabber.html,http://www.qsl.net/ea8bvp/hf2.jpg 9 | G0FTD-QRSS-Kaleidoscope,G0FTD,R&D only,Andy,Kent,http://www.qsl.net/g0ftd/grabber.htm,http://www.qsl.net/g0ftd/hf1.jpg 10 | G0MQW-1,G0MQW,1 (10m),Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,http://www.qsl.net/g0mqw/hf1.jpg 11 | G0MQW-2,G0MQW,2 (Variable),Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,http://www.qsl.net/g0mqw/hf2.jpg 12 | G0MQW-3,G0MQW,3 (Variable),Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,http://www.qsl.net/g0mqw/hf3.jpg 13 | G0MQW-4,G0MQW,4 (Variable),Chris,"Reading, Berkshire, England",http://www.qsl.net/g0mqw/,http://www.qsl.net/g0mqw/hf4.jpg 14 | G0UPL,G0UPL,Usually 20m,Hans,"Turkey",http://www.hanssummers.com,http://www.hanssummers.com/images/stories/balloons/ve3kcl/s.jpg 15 | G3VYZ-Fast-10m,G3VYZ,Grabber 1,Les,"Holywell, Northumberland, UK",http://www.holywell44.com/qrss/qrss.htm,http://www.holywell44.com/qrss/qrss_.jpg 16 | G3VYZ-Fast-40m,G3VYZ,Grabber 3,Les,"Holywell, Northumberland, UK",http://www.holywell44.com/qrss/qrss.htm,http://www.holywell44.com/qrss/qrss_3.jpg 17 | G3VYZ-Fast-80m,G3VYZ,Grabber 2,Les,"Holywell, Northumberland, UK",http://www.holywell44.com/qrss/qrss.htm,http://www.holywell44.com/qrss/qrss_2.jpg 18 | G3VYZ-40m-Slow,G3VYZ,Grabber 4,Les,"Holywell, Northumberland, UK",http://www.holywell44.com/qrss/qrss.htm,http://www.holywell44.com/qrss/qrss_4.jpg 19 | # G3VYZ-5,G3VYZ,Grabber 5,Les,"Holywell, Northumberland, UK",http://www.holywell44.com/qrss/qrss.htm,http://www.holywell44.com/qrss/qrss_5.jpg 20 | G3VYZ-80m-Slow,G3VYZ,Grabber 6,Les,"Holywell, Northumberland, UK",http://www.holywell44.com/qrss/qrss.htm,http://www.holywell44.com/qrss/qrss_6.jpg 21 | G3YXM-136Khz,G3YXM,136Khz band,Dave,England,http://wireless.org.uk/,http://g3yxm.ddns.net/grabs/136.jpg 22 | G3YXM-472Khz,G3YXM,600m bandscope,Dave,England,http://wireless.org.uk/,http://g3yxm.ddns.net/grabs/472.jpg 23 | G4IOG-10m,G4IOG,28MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,http://qsl.net/g4iog/10mdir/hf1.png 24 | G4IOG-20m,G4IOG,14MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,https://www.qsl.net/g4iog/20mdir/hf2.png 25 | G4IOG-30m,G4IOG,10MHz,Bob,"Newington, Kent, England",https://www.qsl.net/g4iog/30mdir/30mpage.html,https://www.qsl.net/g4iog/30mdir/hf3.png 26 | G4IOG-40m,G4IOG,7MHz,Bob,"Newington, Kent, England",http://qsl.net/g4iog,https://www.qsl.net/g4iog/40mdir/hf4.png 27 | G4IOG-80m,G4IOG,3.5MHz,Bob,"Newington, Kent, England",https://www.qsl.net/g4iog/80mdir/80mpage.html,https://www.qsl.net/g4iog/80mdir/hf8.png 28 | G4JVF,G4JVF,Various bands,Philip,"North Derbyshire, UK",http://www.qsl.net/g/g4jvf/,http://www.qsl.net/g/g4jvf/hf4.jpg 29 | GM4SFW-0,GM4SFW,Testing,Hamish,Scotland,https://www.qsl.net/gm4sfw/,https://www.qsl.net/gm4sfw/latest.png 30 | GM4SFW-1,GM4SFW,Testing,Hamish,Scotland,https://www.qsl.net/gm4sfw/,https://www.qsl.net/gm4sfw/10_12metres.png 31 | GM4SFW-2,GM4SFW,Testing,Hamish,Scotland,https://www.qsl.net/gm4sfw/,https://www.qsl.net/gm4sfw/30metre.png 32 | G6AVK,G6AVK,Various bands,Colin,"Essex, England",http://www.qsl.net/g6avk/,http://www.qsl.net/g6avk/hf6.jpg 33 | G6NHU,G6NHU,Various bands,Keith,England,http://g6nhu.co.uk,http://g6nhu.co.uk/images/capture.jpg 34 | GJ7RWT,GJ7RWT,Usually 40m,Andy,"Jersey, England",http://www.qsl.net/gj7rwt/gj7rwthomepage.html,https://www.qsl.net/g/gj7rwt/hf1.jpg 35 | HB9FXX-A,HB9FXX,Variable check band,Martin,Switzerland,https://www.qsl.net/hb9fxx/,https://www.qsl.net/hb9fxx/hf1.png 36 | HB9FXX-B,HB9FXX,Variable check band,Martin,Switzerland,https://www.qsl.net/hb9fxx/,https://www.qsl.net/hb9fxx/hf2.png 37 | HB9FXX-C,HB9FXX,Variable check band,Martin,Switzerland,https://www.qsl.net/hb9fxx/,https://www.qsl.net/hb9fxx/hf3.png 38 | HB9FXX-D,HB9FXX,Variable check band,Martin,Switzerland,https://www.qsl.net/hb9fxx/,https://www.qsl.net/hb9fxx/hf4.png 39 | K4RCG-1,K4RCG,Variable bands,Bob,Blue Rdg Mtns VA – FM08si,https://www.qsl.net/k4rcg/,https://www.qsl.net/k4rcg/hf1.jpg 40 | K4RCG-2,K4RCG,Variable bands,Bob,Blue Rdg Mtns VA – FM08si,https://www.qsl.net/k4rcg/,https://www.qsl.net/k4rcg/hf2.jpg 41 | K4RCG-3,K4RCG,Variable bands,Bob,Blue Rdg Mtns VA – FM08si,https://www.qsl.net/k4rcg/,https://www.qsl.net/k4rcg/latest.jpg 42 | K5MO-1,K5MO,EU/US bands,John, North Carolina,https://qsl.net/k5mo/,https://qsl.net/k5mo/hf3.jpg 43 | K5MO-HIFER,K5MO,22M band,John, North Carolina,https://qsl.net/k5mo/,https://www.qsl.net/k5mo/hifer.jpg 44 | KL7L-0,KL7L,Band can vary,Laurence,"Wasilla, Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/20aAlaska00000.jpg 45 | KL7L-1,KL7L,Band can vary,Laurence,"Wasilla, Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/20Alaska00000.jpg 46 | KL7L-2,KL7L,Band can vary,Laurence,"Wasilla, Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/30Alaska00000.jpg 47 | KL7L-3,KL7L,Band can vary,Laurence,"Wasilla, Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/40Alaska00000.jpg 48 | KL7L-4,KL7L,136Khz,Laurence,"Wasilla, Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/2LF00000.jpg 49 | KL7L-5,KL7L,Band can vary,Laurence,"Wasilla, Alaska",http://kl7l.com/Alaska.html,http://kl7l.com/30aAlaska00000.jpg 50 | LA5GOA-30m,LA5GOA,30m,Steen,Norway,http://la5goa.manglet.net/grabber,https://www.qsl.net/la5goa/lopshot1.jpg 51 | M0BMN,M0BMN,Various, Paul,Wolverhampton,http://www.phoenixkitsonline.co.uk,http://www.phoenixkitsonline.co.uk/qrss.jpg 52 | M0HGU,M0HGU,Various, Nick,Norfolk UK,https://www.dctower.co.uk/,https://www.dctower.co.uk/qrss/30mgrab-.jpg 53 | M0GBZ,M0GBZ,Various part time,Euan,Southern UK,https://qsl.net/m0gbz/,https://qsl.net/m0gbz/hf1.jpg 54 | # M0RON,M0RON,Testing,Andy,Bishops Cleeve,https://www.qsl.net/m0ron/,https://www.qsl.net/m0ron/grabs/hf1.png 55 | N9JL-1,N9JL,30m FSKVIEW,John,Shorewood IL,https://www.qsl.net/n9jl,https://www.qsl.net/n9jl/capt.jpg 56 | N8NJ-1,N8NJ,Various bands,Larry,Ohio,https://www.qsl.net/n8nj/index.html,https://www.qsl.net/n8nj/hf1.jpg 57 | N8NJ-2,N8NJ,Various bands,Larry,Ohio,https://www.qsl.net/n8nj/index.html,https://www.qsl.net/n8nj/hf2.jpg 58 | # OH8GKP-0,OH8GKP,Experimental,Heikki,"Liminka,Finland",https://qsl.net/oh8gkp/wspr.html,https://qsl.net/oh8gkp/latest.png 59 | # OH8GKP-1,OH8GKP,Experimental,Heikki,"Liminka,Finland",https://qsl.net/oh8gkp/wspr.html,https://qsl.net/oh8gkp/oh8gkp1.gif 60 | # OH8GKP-2,OH8GKP,Experimental,Heikki,"Liminka,Finland",https://qsl.net/oh8gkp/qrss/wspr.html,https://qsl.net/oh8gkp/qrss/latest.png 61 | # OH8GKP-3,OH8GKP,Experimental,Heikki,"Liminka,Finland",https://qsl.net/oh8gkp/qrss/wspr.html,https://qsl.net/oh8gkp/qrss/oh8gkp.gif 62 | OK1FCX-1,OK1FCX,Experimental,Radovan,"Kamenny Privoz, Czech Republic",http://www.qsl.net/ok1fcx/,http://www.qsl.net/ok1fcx/hf1.jpg 63 | OK1FCX-2,OK1FCX,Experimental,Radovan,"Kamenny Privoz, Czech Republic",http://www.qsl.net/ok1fcx/,http://www.qsl.net/ok1fcx/hf2.jpg 64 | OK1FCX-3-80m-NVIS,OK1FCX,Experimental,Radovan,"Kamenny Privoz, Czech Republic",http://www.qsl.net/ok1fcx/,http://www.qsl.net/ok1fcx/hf3.jpg 65 | ON4CDJ,ON4CDJ,Various,Patrick,Belgium,http://qsl.net/on4cdj/qrss,http://qsl.net/on4cdj/qrss/hf1.jpg 66 | PA2OHH-10m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf5.jpg 67 | PA2OHH-20m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf4.jpg 68 | PA2OHH-30m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf1.jpg 69 | PA2OHH-40m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf2.jpg 70 | PA2OHH-80m,PA2OHH,Backup system Lopora,Onno,Spain,http://www.qsl.net/pa2ohh/grabber.htm,https://www.qsl.net/pa2ohh/grabber/hf3.jpg 71 | S52AS-30m,S52AS,30m Part time,Roman,"Novo Mesto, Slovenia",http://novomesto.zevs.si/graber.htm,http://novomesto.zevs.si/podatki/test.gif 72 | SA6BSS-1,SA6BSS,10 MHz,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf1.jpg 73 | SA6BSS-2,SA6BSS,7 MHz,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf2.jpg 74 | SA6BSS-3,SA6BSS,14 MHz,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf3.jpg 75 | SA6BSS-4,SA6BSS,Variable,Mikael,Sweden,http://www.qsl.net/sa6bss,http://www.qsl.net/sa6bss/hf4.jpg 76 | TF3HZ-1,TF3HZ,Testing,Haldor,Iceland,https://halldorg.is/,https://dl.dropboxusercontent.com/s/ldztb0dsnatol3y/HF2%20grabber.jpg 77 | VA3ROM-30m,VA3ROM,30m,Robert,"Ontario, Canada",http://va3rom.com/QRSS/QRSS.html,http://va3rom.com/QRSS/image1.jpg 78 | VE1VDM-1,VE1VDM,Various,Vernon,"Nova Scotia, Canada",https://www.qsl.net/ve1vdm/,https://www.qsl.net/ve1vdm/argo.jpg 79 | VE1VDM-2,VE1VDM,Various,Vernon,"Nova Scotia, Canada",https://www.qsl.net/ve1vdm/,https://www.qsl.net/ve1vdm/argo1.jpg 80 | VE3GTC,VE3GTC,40m,Graham,"Ontario, Canada",http://users.aei.ca/~planophore/grabber,https://dl.dropboxusercontent.com/s/iz4s0yhwqvtge8p/picture.jpg 81 | VK3EDW,VK3EDW,Primary,John,Kallista - Victoria,http://www.qsl.net/vk3edw,http://www.qsl.net/vk3edw/QRSS/assets/images/hf1.jpg 82 | VE7IGH-30m,VE7IGH,10Mhz & attic antenna,Gregory,"British Columbia, Canada",http://www.qsl.net/ve7igh/Grabber.htm,http://www.qsl.net/ve7igh/capt.jpg 83 | W1BW-80m,W1BW,80m,Bruce,"QTH Boston,Mass",http://w1bw.us/grabber/,http://w1bw.us/grabber/3/latest.jpg 84 | W1BW-60m,W1BW,60m,Bruce,"QTH Boston,Mass",http://w1bw.us/grabber/,http://w1bw.us/grabber/5/latest.jpg 85 | W1BW-40m,W1BW,40m,Bruce,"QTH Boston,Mass",http://w1bw.us/grabber/,http://w1bw.us/grabber/7/latest.jpg 86 | W1BW-30m,W1BW,30m,Bruce,"QTH Boston,Mass",http://w1bw.us/grabber/,http://w1bw.us/grabber/10/latest.jpg 87 | W1BW-20m,W1BW,20m,Bruce,"QTH Boston,Mass",http://w1bw.us/grabber/,http://w1bw.us/grabber/14/latest.jpg 88 | W1BW-17m,W1BW,17m,Bruce,"QTH Boston,Mass",http://w1bw.us/grabber/,http://w1bw.us/grabber/18/latest.jpg 89 | W1BW-15m,W1BW,15m,Bruce,"QTH Boston,Mass",http://w1bw.us/grabber/,http://w1bw.us/grabber/21/latest.jpg 90 | W1BW-12m,W1BW,12m,Bruce,"QTH Boston,Mass",http://w1bw.us/grabber/,http://w1bw.us/grabber/24/latest.jpg 91 | W1BW-10m,W1BW,10m,Bruce,"QTH Boston,Mass",http://w1bw.us/grabber/,http://w1bw.us/grabber/28/latest.jpg 92 | WD4AH,WD4AH,Testing,Alfred,Florida,http://www.qsl.net/wd4ah,http://www.qsl.net/wd4ah/hf1.jpg 93 | WD4ELG-10-Fast,WD4ELG,10m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://dl.dropboxusercontent.com/s/qhmkogils6jly3a/WD4ELG%2010M%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 94 | WD4ELG-20-Fast,WD4ELG,20m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://dl.dropboxusercontent.com/s/gba72cz0au66032/WD4ELG%2020M%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 95 | WD4ELG-30-Fast,WD4ELG,30m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://dl.dropboxusercontent.com/s/7djby65cbfh6hv7/WD4ELG%2030M%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 96 | WD4ELG-40-Fast,WD4ELG,40m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://dl.dropboxusercontent.com/s/ajhc4t640k7k67u/WD4ELG%2040M%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 97 | WD4ELG-80-Fast,WD4ELG,80m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://dl.dropboxusercontent.com/s/59ktcp48iie5i1m/WD4ELG%2080M%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 98 | WD4ELG-160-Fast,WD4ELG,160m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://www.dropbox.com/s/qi6jp3ictzqd0iq/WD4ELG%20160M%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 99 | WD4ELG-10-Slow,WD4ELG,10m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://www.dropbox.com/s/htkrgr1r41cgdcw/WD4ELG%2010M%206%20Hour%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 100 | WD4ELG-20-Slow,WD4ELG,20m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://www.dropbox.com/s/e0sysy2suwwnqbo/WD4ELG%2020M%206%20Hour%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 101 | WD4ELG-30-Slow,WD4ELG,30m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://www.dropbox.com/s/5tyln8l02oscbp6/WD4ELG%2030M%206%20Hour%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 102 | WD4ELG-40-Slow,WD4ELG,40m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://www.dropbox.com/s/5bllpsdcvd0iwqt/WD4ELG%2040M%206%20Hour%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 103 | WD4ELG-80-Slow,WD4ELG,80m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://www.dropbox.com/s/rrpil5x08v3e3s2/WD4ELG%2080M%206%20Hour%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 104 | WD4ELG-160-Slow,WD4ELG,80m grabber,Mark,"Greensboro, NC",http://wd4elg.blogspot.com,https://www.dropbox.com/s/b6mr1461h9ym0uq/WD4ELG%20160M%206%20Hour%20grabber%20%28REFRESH%20for%20latest%20grab%29.jpg 105 | WY7BUD-1,WY7BUD,30m grabber,Dave,"Cheyenne-Wyoming",https://www.qsl.net/wy7bud/,https://www.qsl.net/wy7bud/hf1.jpg 106 | W4HBK-1,W4HBK,10min,Bill,"Pensacola, Florida",http://www.qsl.net/w4hbk/,http://www.qsl.net/w4hbk/hf1.jpg 107 | W4HBK-3,W4HBK,Auxillary grabber,Bill,"Pensacola, Florida",http://www.qsl.net/w4hbk/,https://www.qsl.net/w4hbk/aux1.jpg 108 | W4HBK-Experimental,W4HBK,Experimental tests,Bill,"Pensacola, Florida",http://www.qsl.net/w4hbk/,http://www.qsl.net/w4hbk/hf3.jpg 109 | W6REK-20m,W6REK,20m,Henry,"San Jose, California",http://yak.net/qrss/,http://yak.net/qrss/W6REK-20m.png 110 | W6REK-30m,W6REK,30m,Henry,"San Jose, California",http://yak.net/qrss/,http://yak.net/qrss/W6REK-30m.png 111 | W6REK-40m,W6REK,40m,Henry,"San Jose, California",http://yak.net/qrss/,http://yak.net/qrss/W6REK-40m.png 112 | W6REK-80j,W6REK,80j,Henry,"San Jose, California",http://yak.net/qrss/,http://yak.net/qrss/W6REK-80j.png 113 | WA5DJJ-6m,WA5DJJ,6m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf9.jpg 114 | WA5DJJ-10m,WA5DJJ,10m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf8.jpg 115 | WA5DJJ-Wide-10m,WA5DJJ,10m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,https://qsl.net/wa5djj/uhf7.jpg 116 | WA5DJJ-12m,WA5DJJ,12m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf7.jpg 117 | WA5DJJ-15m,WA5DJJ,15m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf6.jpg 118 | WA5DJJ-17m,WA5DJJ,17m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf5.jpg 119 | WA5DJJ-20m,WA5DJJ,20m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf4.jpg 120 | WA5DJJ-22m-Hifer1,WA5DJJ,22m HiFer,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,https://qsl.net/wa5djj/uhf4.jpg 121 | WA5DJJ-22m-Hifer2,WA5DJJ,22m HiFer,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,https://qsl.net/wa5djj/vlf1.jpg 122 | WA5DJJ-30m,WA5DJJ,30m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf3.jpg 123 | WA5DJJ-40m,WA5DJJ,40m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/hf2.jpg 124 | WA5DJJ-80m,WA5DJJ,80m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/mf3.jpg 125 | WA5DJJ-160m,WA5DJJ,160m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,http://qsl.net/wa5djj/mf2.jpg 126 | WA5DJJ-630m,WA5DJJ,630m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,https://qsl.net/wa5djj/mf1.jpg 127 | WA5DJJ-2200m,WA5DJJ,2200m,David,"Las Cruces, New Mexico",http://qsl.net/wa5djj/,https://qsl.net/wa5djj/mf4.jpg 128 | ZL2IK1,ZL2IK,10min,Pete,"Northland, New Zealand",http://zl2ik.com/Grabber.html,http://zl2ik.com/Argo.jpg 129 | ZL2IK2,ZL2IK,5hr,Pete,"Northland, New Zealand",http://zl2ik.com/Grabber4Hr.html,http://zl2ik.com/Brgo.jpg 130 | -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/data/grabs/G0MQW-4 2021.04.28.21.22.00.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/src/backend/QrssPlusTests/data/grabs/G0MQW-4 2021.04.28.21.22.00.jpg -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/data/grabs/K5MO-1 2021.04.28.15.42.00.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/src/backend/QrssPlusTests/data/grabs/K5MO-1 2021.04.28.15.42.00.jpg -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/data/grabs/KL7L-0 2021.04.28.16.32.00.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/src/backend/QrssPlusTests/data/grabs/KL7L-0 2021.04.28.16.32.00.jpg -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/data/grabs/PA2OHH-40M 2021.04.28.19.22.00.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/src/backend/QrssPlusTests/data/grabs/PA2OHH-40M 2021.04.28.19.22.00.jpg -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/data/grabs/W6REK-40M 2021.04.28.15.42.00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/src/backend/QrssPlusTests/data/grabs/W6REK-40M 2021.04.28.15.42.00.png -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/data/grabs/demo.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/src/backend/QrssPlusTests/data/grabs/demo.bmp -------------------------------------------------------------------------------- /src/backend/QrssPlusTests/data/grabs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/src/backend/QrssPlusTests/data/grabs/demo.gif -------------------------------------------------------------------------------- /src/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `yarn build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrssplus", 3 | "version": "0.2.0", 4 | "homepage": "./", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.14.1", 8 | "@testing-library/react": "^12.0.0", 9 | "@testing-library/user-event": "^13.2.1", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-modal-image": "^2.5.0", 13 | "react-scripts": "5.0.0", 14 | "web-vitals": "^2.1.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/frontend/public/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/src/frontend/public/banner.jpg -------------------------------------------------------------------------------- /src/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/QRSSplus/13b2e95c87dbf31aadf2a086ca11850132191960/src/frontend/public/favicon.ico -------------------------------------------------------------------------------- /src/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | QRSS Plus 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import GrabberList from './components/GrabberList'; 3 | import Config from './components/Config'; 4 | import News from './components/News'; 5 | import Thumbnails from './components/Thumbnails'; 6 | import MobileView from './components/MobileView'; 7 | import Banner from './components/Banner'; 8 | 9 | function App() { 10 | 11 | const [grabberStats, setGrabberStats] = useState(); 12 | const [thumbnailCount, setThumbnailCount] = useState(12); 13 | const [isStichVisible, setIsStichVisible] = useState(false); 14 | 15 | useEffect(() => { 16 | // this block runs once at startup 17 | updateGrabbers(); 18 | }, []); 19 | 20 | const updateGrabbers = () => { 21 | console.log("UPDATING GRABBERS " + new Date().toISOString()); 22 | const url = 'https://qrssplus.z20.web.core.windows.net/grabbers.json?nocache=' + (new Date()).getTime(); 23 | fetch(url, { 'cache': 'no-store', 'Cache-Control': 'no-cache' }) 24 | .then(response => response.json()) 25 | .then(obj => { 26 | setGrabberStats(obj); 27 | console.log(`read ${Object.keys(obj.grabbers).length} grabbers at ${obj.created}`); 28 | }); 29 | } 30 | 31 | const urlParams = new URLSearchParams(window.location.search); 32 | const view = urlParams.get('view'); 33 | switch (view) { 34 | case "mobile": 35 | return ( 36 |
37 | 38 | 39 |
40 | ) 41 | default: 42 | return ( 43 |
44 |
45 | 46 | 47 | 48 | 49 | 54 |
55 |
56 |
57 | QRSS Plus 58 |
59 |
60 | by Scott Harden (AJ4VD) 61 |
62 | 65 |
66 |
67 | ); 68 | } 69 | } 70 | 71 | export default App; -------------------------------------------------------------------------------- /src/frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders app without crashing', () => { 5 | render(); 6 | }); 7 | -------------------------------------------------------------------------------- /src/frontend/src/components/Banner.js: -------------------------------------------------------------------------------- 1 | function Banner(props) { 2 | 3 | const switchToDesktop = () => { 4 | window.open("./"); 5 | } 6 | 7 | const mobile = () => { 8 | return ( 9 |
10 |
QRSS Plus
11 |
12 | 13 | 16 |
17 |
18 | ) 19 | } 20 | 21 | const desktop = () => { 22 | return ( 23 |
24 |

QRSS Plus

25 |

Automatically Updating Active QRSS Grabbers List

26 | 29 |
30 | ) 31 | } 32 | 33 | return props.mobile ? mobile() : desktop(); 34 | } 35 | 36 | export default Banner -------------------------------------------------------------------------------- /src/frontend/src/components/Config.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | function Config(props) { 4 | 5 | const [timestamp, setTimestamp] = useState(); 6 | 7 | const setThumbnailCount = props.setThumbnailCount; 8 | const setIsStichVisible = props.setIsStichVisible; 9 | const grabberStats = props.grabberStats; 10 | 11 | const handleThumbChange = (event) => { 12 | const value = event.target.value; 13 | if (value === "2hr") 14 | setThumbnailCount(6 * 2); 15 | else if (value === "8hr") 16 | setThumbnailCount(6 * 8); 17 | else 18 | setThumbnailCount(0); 19 | } 20 | 21 | const handleStitchChange = (event) => { 22 | setIsStichVisible(event.target.checked); 23 | } 24 | 25 | useEffect(() => { 26 | 27 | const updateTimestamp = () => { 28 | const dt = new Date(); 29 | const ts = leftPad(dt.getUTCHours()) + ":" + leftPad(dt.getUTCMinutes()) + ":" + leftPad(dt.getUTCSeconds()); 30 | setTimestamp(ts); 31 | } 32 | 33 | updateTimestamp(); 34 | const interval = setInterval(() => { updateTimestamp(); }, 1000); 35 | return () => clearInterval(interval); 36 | }, []); 37 | 38 | 39 | const leftPad = (num, size = 2, padChar = "0") => { 40 | num = num.toString(); 41 | while (num.length < size) 42 | num = padChar + num; 43 | return num; 44 | }; 45 | 46 | const grabberDataAgeMessage = () => { 47 | if (!grabberStats) 48 | return; 49 | const updatedDate = new Date(grabberStats.created); 50 | const updatedLinuxTime = updatedDate.getTime() / 1000; 51 | const nowDate = new Date(); 52 | const nowLinuxTime = nowDate.getTime() / 1000; 53 | const urlAgeMin = (nowLinuxTime - updatedLinuxTime) / 60; 54 | return `Last updated on ${updatedDate.toLocaleDateString()} ` + 55 | `at ${updatedDate.toLocaleTimeString()} ` + 56 | `(${Math.round(urlAgeMin)} min ago)`; 57 | } 58 | 59 | return ( 60 |
61 | 62 | 69 | 70 | 76 | 77 |
78 |
Settings
79 | 80 |
81 |
UTC Time:
82 |
{timestamp}
83 |
84 | 85 |
86 |
Thumbnails:
87 | 92 |
93 | 94 |
95 |
Stitch:
96 |   8hr 97 |
98 |
99 | 100 |
101 | {grabberDataAgeMessage()} 102 |
103 | 104 |
105 | ); 106 | } 107 | 108 | export default Config; -------------------------------------------------------------------------------- /src/frontend/src/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | const Dashboard = (props) => { 5 | 6 | if (!props.grabberStats || Object.keys(props.grabberStats).length === 0) { 7 | return (
Loading dashboard...
); 8 | } 9 | 10 | const grabbers = props.grabberStats.grabbers; 11 | 12 | const grabberRowClass = (grabber) => { 13 | if (grabber.urls.length > 0) 14 | return "" 15 | if (grabber.lastUniqueAgeDays >= 7) 16 | return "table-danger" 17 | else return "table-warning" 18 | } 19 | 20 | const grabberAgeMessage = (grabber) => { 21 | if (grabber.lastUniqueAgeDays < 1){ 22 | return "today"; 23 | } else if (grabber.lastUniqueAgeDays < 999) { 24 | return grabber.lastUniqueAgeDays + " days ago"; 25 | } else { 26 | return "never"; 27 | } 28 | } 29 | 30 | const grabCountMessage = (grabber) => 31 | (grabber.urls.length > 0) 32 | ? grabber.urls.length 33 | : (--); 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | { 51 | Object.keys(grabbers) 52 | .map(id => (grabbers[id])) 53 | .map(grabber => ( 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | )) 65 | } 66 | 67 |
IDNameCallsignLocationImageWebsiteGrabsLast Upload
{grabber.id}{grabber.name}{grabber.callsign}{grabber.location}imagewebsite{grabCountMessage(grabber)}{grabberAgeMessage(grabber)}
68 | ) 69 | } 70 | 71 | export default Dashboard; -------------------------------------------------------------------------------- /src/frontend/src/components/GrabberDetails.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ModalImage from "react-modal-image"; 4 | 5 | const GrabberDetails = (props) => { 6 | 7 | const grabber = props.grabber; 8 | const showStitch = props.showStitch; 9 | const maxThumbnailCount = props.maxThumbnailCount; 10 | 11 | const latestUrl = grabber.urls[grabber.urls.length - 1]; 12 | 13 | const renderActivityIcon = (ageMinutes) => { 14 | if (ageMinutes < 35) 15 | return (Active) 16 | if (ageMinutes < (60 * 24)) 17 | return (Inactive ({ageMinutes} minutes)) 18 | return (Offline ({ageMinutes / 60 / 24} days)) 19 | }; 20 | 21 | const renderDatedThumbnail = (url) => { 22 | return ( 23 |
24 |
{timestampFromUrl(url)} ({timestampAgeFromUrl(url)})
25 |
26 | 31 |
32 |
33 | ) 34 | } 35 | 36 | const renderPrimaryImage = (latestUrl) => { 37 | return ( 38 |
39 |
{timestampFromUrl(latestUrl)} ({timestampAgeFromUrl(latestUrl)})
40 |
41 |
42 | 47 |
48 |
49 |
50 | ); 51 | } 52 | 53 | const basename = (url) => { 54 | return url.substr(url.lastIndexOf('/') + 1); 55 | } 56 | 57 | const timestampFromUrl = (url) => { 58 | const parts = basename(url).split(" ")[1].split('.'); 59 | const timestamp = parts[3] + ":" + parts[4]; 60 | return timestamp; 61 | } 62 | 63 | const timestampAgeFromUrl = (url) => { 64 | const parts = basename(url).split(" ")[1].split('.'); 65 | const urlDate = new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]); 66 | const urlLinuxTime = urlDate.getTime() / 1000 - urlDate.getTimezoneOffset() * 60; 67 | const nowDate = new Date(); 68 | const nowLinuxTime = nowDate.getTime() / 1000; 69 | const urlAgeMin = (nowLinuxTime - urlLinuxTime) / 60; 70 | return `${Math.round(urlAgeMin)} min`; 71 | } 72 | 73 | return ( 74 |
75 |

{grabber.id}

76 | 77 |
78 | {grabber.name} ({grabber.callsign}) 79 | in {grabber.location} (website) 80 |   81 | {renderActivityIcon(grabber.lastUniqueAgeMinutes)} 82 |
83 | 84 | { 85 | renderPrimaryImage(latestUrl) 86 | } 87 | 88 | { 89 | Object.keys(grabber.urls) 90 | .map(x => grabber.urls[x]) 91 | .reverse() 92 | .slice(0, maxThumbnailCount) 93 | .map(x => renderDatedThumbnail(x)) 94 | } 95 | 96 | { 97 | showStitch ? ( 98 |
99 | { 100 | Object.keys(grabber.urls) 101 | .map(x => grabber.urls[x]) 102 | .map(url => ( 103 |
104 | 109 |
110 | )) 111 | } 112 |
113 | ) : "" 114 | } 115 | 116 |
117 | ); 118 | } 119 | 120 | export default GrabberDetails; -------------------------------------------------------------------------------- /src/frontend/src/components/GrabberList.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import GrabberDetails from './GrabberDetails'; 4 | 5 | const GrabberList = (props) => { 6 | 7 | if (!props.grabberStats || Object.keys(props.grabberStats).length === 0) { 8 | return (
Loading grabbers...
); 9 | } 10 | 11 | const grabbers = props.grabberStats.grabbers; 12 | const maxGrabberCount = props.maxGrabberCount; 13 | const thumbnailCount = props.thumbnailCount; 14 | const isStichVisible = props.isStichVisible; 15 | 16 | return ( 17 | Object.keys(grabbers) 18 | .filter(id => grabbers[id].urls.length > 0) 19 | .slice(0, maxGrabberCount) 20 | .map(id => 21 | 27 | ) 28 | ); 29 | } 30 | 31 | export default GrabberList; -------------------------------------------------------------------------------- /src/frontend/src/components/MobileView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ModalImage from "react-modal-image"; 3 | 4 | function MobileView(props) { 5 | 6 | if (!props.grabberStats || Object.keys(props.grabberStats).length === 0) { 7 | return (
Loading grabbers...
); 8 | } 9 | 10 | const grabbers = props.grabberStats.grabbers; 11 | 12 | const getImage = (grabber) => { 13 | const latestUrl = grabber.urls[grabber.urls.length - 1]; 14 | const basename = latestUrl.substr(latestUrl.lastIndexOf('/') + 1); 15 | return ( 16 |
17 |
18 | {grabber.id} 19 |
20 |
21 | 26 |
27 |
28 | ) 29 | } 30 | 31 | const activeGrabbers = Object.keys(grabbers).filter(id => grabbers[id].urls.length > 0) 32 | const totalGrabberCount = Object.keys(grabbers).length; 33 | 34 | return ( 35 | <> 36 |
Active Grabbers ({activeGrabbers.length} of {totalGrabberCount})
37 | { 38 | Object.keys(grabbers) 39 | .filter(id => grabbers[id].urls.length > 0) 40 | .map(id => getImage(grabbers[id])) 41 | } 42 | 43 | ) 44 | } 45 | 46 | export default MobileView; -------------------------------------------------------------------------------- /src/frontend/src/components/News.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Dashboard from './Dashboard'; 3 | 4 | function News(props) { 5 | 6 | const [alertMessage, setAlertMessage] = useState(); 7 | const [alertDate, setAlertDate] = useState(); 8 | 9 | useEffect(() => { 10 | console.log("fetching NOAA alert message...") 11 | fetch('https://services.swpc.noaa.gov/text/wwv.txt') 12 | .then(response => response.text()) 13 | .then(text => { 14 | const lines = text.split("\n"); 15 | 16 | const dt = lines[1].replace(":Issued: ", ""); 17 | setAlertDate(dt); 18 | 19 | const goodLines = lines.filter(x => !x.startsWith(":")).filter(x => !x.startsWith("#")); 20 | const goodMessage = goodLines.join("\n"); 21 | setAlertMessage(goodMessage); 22 | }); 23 | }, []); 24 | 25 | return ( 26 |
27 | 28 |
29 | 38 | 39 | 48 | 49 | 58 | 59 | 68 | 69 | 73 | Mobile View 74 | 75 | 76 |
77 | 78 |
79 |
80 |
81 |
82 | Geophysical Alert Message ({alertDate}) 83 |
84 |
85 |
{alertMessage}
86 |
87 |
88 |
89 | 90 |
91 |
92 |
93 | NOAA Aurora Forecast 94 |
95 |
96 |
97 | 99 | northern-hemisphere 101 | 102 |
103 |
104 | 106 | southern-hemisphere 108 | 109 |
110 |
111 |
112 |
113 | 114 |
115 |
116 |
117 | Sunlit Map with MUF Data 118 |
119 | 120 | sunlit map 122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 | Grabber Dashboard 130 |
131 | 132 |
133 |
134 | 135 |
136 | 137 |
138 | ) 139 | } 140 | 141 | export default News; -------------------------------------------------------------------------------- /src/frontend/src/components/Thumbnails.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | const Thumbnails = (props) => { 5 | 6 | if (!props.grabberStats || Object.keys(props.grabberStats).length === 0) { 7 | return (
Loading thumbnails...
); 8 | } 9 | 10 | const grabbers = props.grabberStats.grabbers; 11 | 12 | const getThumbnail = (grabber) => { 13 | return ( 14 |
15 |
{grabber.id}
16 |
17 | 18 | {grabber.id} 24 | 25 |
26 |
27 | ); 28 | } 29 | 30 | const activeGrabbers = Object.keys(grabbers).filter(id => grabbers[id].urls.length > 0); 31 | 32 | return ( 33 |
34 |

Active Grabbers ({activeGrabbers.length} of {Object.keys(grabbers).length})

35 | {activeGrabbers.map(id => getThumbnail(grabbers[id]))} 36 |
37 | ); 38 | } 39 | 40 | export default Thumbnails; -------------------------------------------------------------------------------- /src/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // If you want to start measuring performance in your app, pass a function 14 | // to log results (for example: reportWebVitals(console.log)) 15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 16 | reportWebVitals(); 17 | -------------------------------------------------------------------------------- /src/frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | --------------------------------------------------------------------------------