diff --git a/.dockerignore b/.dockerignore
index 38f9a76..b233715 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,16 +1,8 @@
-# Docker
-.dockerignore
-Dockerfile
-
-# CPython compiler output
-__pycache__
-*.pyc
-
-# Python package stuff
-reqlib-metadata
-pyproject.toml
-*.egg-info
-
-# VS Code
-.vscode
-
+**/bin
+**/obj
+**/out
+**/.vscode
+**/.vs
+**/omnisharp.json
+.dotnet
+.Microsoft.DotNet.ImageBuilder
\ No newline at end of file
diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml
index 227e702..ec7f2c2 100644
--- a/.github/workflows/build-image.yml
+++ b/.github/workflows/build-image.yml
@@ -19,5 +19,6 @@ jobs:
- name: Publish
uses: docker/build-push-action@v2
with:
+ context: .
tags: ${{ format('ghcr.io/{0}/new_infofer_scraper:latest', github.actor) }}
push: true
diff --git a/.gitignore b/.gitignore
index 19fe82c..8bf1b90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,448 @@
-# CPython compiler output
-__pycache__
-*.pyc
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
-# Python package stuff
-reqlib-metadata
-pyproject.toml
-*.egg-info
+# Tye
+.tye/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*[.json, .xml, .info]
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
-# VS Code
-.vscode
+# Ionide - VsCode extension for F# Support
+.ionide/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+##
+## Visual Studio Code
+##
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..aeb8ac6
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,46 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "ConsoleTest",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "buildConsoleTest",
+ "program": "${workspaceFolder}/ConsoleTest/bin/Debug/net6.0/ConsoleTest.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ "stopAtEntry": false,
+ "console": "integratedTerminal"
+ },
+ {
+ // Use IntelliSense to find out which attributes exist for C# debugging
+ // Use hover for the description of the existing attributes
+ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
+ "name": ".NET Core Launch (web)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // If you have changed target frameworks, make sure to update the program path.
+ "program": "${workspaceFolder}/server/bin/Debug/net6.0/Server.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/server",
+ "stopAtEntry": false,
+ // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
+ // "serverReadyAction": {
+ // "action": "openExternally",
+ // "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
+ // },
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "sourceFileMap": {
+ "/Views": "${workspaceFolder}/Views"
+ }
+ },
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..1cb8e13
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,53 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/server/server.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "buildConsoleTest",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/ConsoleTest/ConsoleTest.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "publish",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "publish",
+ "${workspaceFolder}/server/server.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "watch",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "--project",
+ "${workspaceFolder}/server/server.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ConsoleTest/.vscode/launch.json b/ConsoleTest/.vscode/launch.json
new file mode 100644
index 0000000..cd971a6
--- /dev/null
+++ b/ConsoleTest/.vscode/launch.json
@@ -0,0 +1,26 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ // Use IntelliSense to find out which attributes exist for C# debugging
+ // Use hover for the description of the existing attributes
+ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
+ "name": ".NET Core Launch (console)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // If you have changed target frameworks, make sure to update the program path.
+ "program": "${workspaceFolder}/bin/Debug/net5.0/ConsoleTest.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
+ "console": "internalConsole",
+ "stopAtEntry": false
+ },
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ConsoleTest/.vscode/tasks.json b/ConsoleTest/.vscode/tasks.json
new file mode 100644
index 0000000..ac5a31f
--- /dev/null
+++ b/ConsoleTest/.vscode/tasks.json
@@ -0,0 +1,42 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/ConsoleTest.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "publish",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "publish",
+ "${workspaceFolder}/ConsoleTest.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "watch",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "${workspaceFolder}/ConsoleTest.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ConsoleTest/ConsoleTest.csproj b/ConsoleTest/ConsoleTest.csproj
new file mode 100644
index 0000000..4df0a6c
--- /dev/null
+++ b/ConsoleTest/ConsoleTest.csproj
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+ Exe
+ net6.0
+
+
+
diff --git a/ConsoleTest/Program.cs b/ConsoleTest/Program.cs
new file mode 100644
index 0000000..470e4f0
--- /dev/null
+++ b/ConsoleTest/Program.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Text.Json;
+using System.Threading.Tasks;
+using InfoferScraper;
+using InfoferScraper.Scrapers;
+
+while (true) {
+ Console.WriteLine("1. Scrape Train");
+ Console.WriteLine("2. Scrape Station");
+ Console.WriteLine("0. Exit");
+
+ var input = Console.ReadLine()?.Trim();
+ switch (input) {
+ case "1":
+ await PrintTrain();
+ break;
+ case "2":
+ await PrintStation();
+ break;
+ case null:
+ case "0":
+ goto INPUT_LOOP_BREAK;
+ }
+ Console.WriteLine();
+}
+INPUT_LOOP_BREAK:;
+
+async Task PrintTrain() {
+ Console.Write("Train number: ");
+ var trainNumber = Console.ReadLine()?.Trim();
+
+ if (trainNumber == null) {
+ return;
+ }
+
+ Console.WriteLine(
+ JsonSerializer.Serialize(
+ await TrainScraper.Scrape(trainNumber),
+ new JsonSerializerOptions {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = true,
+ }
+ )
+ );
+}
+async Task PrintStation() {
+ Console.Write("Station name: ");
+ var stationName = Console.ReadLine()?.Trim();
+
+ if (stationName == null) {
+ return;
+ }
+
+ Console.WriteLine(
+ JsonSerializer.Serialize(
+ await StationScraper.Scrape(stationName),
+ new JsonSerializerOptions {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = true,
+ }
+ )
+ );
+}
diff --git a/Dockerfile b/Dockerfile
index 364d64a..38bcd9d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,23 +1,23 @@
-FROM python:slim
-
-RUN pip install pipenv
-
-WORKDIR /var/app/scraper
-COPY scraper/Pipfil* ./
-COPY scraper/setup.py ./
-WORKDIR /var/app/server
-COPY server/Pipfil* ./
-RUN pipenv install
-RUN pipenv graph
-
-WORKDIR /var/app/scraper
-COPY scraper .
-WORKDIR /var/app/server
-COPY server .
-RUN rm server/scraper
-RUN ln -s /var/app/scraper ./server/scraper
-
-ENV PORT 5000
-EXPOSE ${PORT}
-
-CMD ["pipenv", "run", "python3", "-m", "main"]
+# https://hub.docker.com/_/microsoft-dotnet
+FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
+WORKDIR /source
+
+# copy csproj and restore as distinct layers
+COPY *.sln .
+COPY server/*.csproj ./server/
+COPY scraper/*.csproj ./scraper/
+COPY ConsoleTest/*.csproj ./ConsoleTest/
+RUN dotnet restore
+
+# copy everything else and build app
+COPY server/. ./server/
+COPY scraper/. ./scraper/
+COPY ConsoleTest/. ./ConsoleTest/
+WORKDIR /source/server
+RUN dotnet publish -c release -o /app --no-restore
+
+# final stage/image
+FROM mcr.microsoft.com/dotnet/aspnet:6.0
+WORKDIR /app
+COPY --from=build /app ./
+ENTRYPOINT ["dotnet", "Server.dll"]
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 71e4f7c..0000000
--- a/Pipfile
+++ /dev/null
@@ -1,11 +0,0 @@
-[[source]]
-url = "https://pypi.org/simple"
-verify_ssl = true
-name = "pypi"
-
-[packages]
-
-[dev-packages]
-
-[requires]
-python_version = "3.9"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 8aee7e8..0000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "a36a5392bb1e8bbc06bfaa0761e52593cf2d83b486696bf54667ba8da616c839"
- },
- "pipfile-spec": 6,
- "requires": {
- "python_version": "3.9"
- },
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {},
- "develop": {}
-}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..d7519b7
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,12 @@
+version: '3'
+
+services:
+ infofer_scraper:
+ image: new_infofer_scraper
+ build: .
+ ports:
+ - ${PORT:-5000}:80
+ environment:
+ - DB_DIR=/data
+ volumes:
+ - ./data:/data
diff --git a/new-infofer-scraper.sln b/new-infofer-scraper.sln
new file mode 100644
index 0000000..bbdb29a
--- /dev/null
+++ b/new-infofer-scraper.sln
@@ -0,0 +1,62 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.6.30114.105
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "scraper", "scraper\scraper.csproj", "{E08BC25C-B39B-40F9-8114-A8D6545EE1C1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server", "server\server.csproj", "{C2D22A33-5317-47A3-B28A-E151224D3E46}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleTest", "ConsoleTest\ConsoleTest.csproj", "{0D8E3B5F-2511-4174-8129-275500753585}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Debug|x64.Build.0 = Debug|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Debug|x86.Build.0 = Debug|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Release|x64.ActiveCfg = Release|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Release|x64.Build.0 = Release|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Release|x86.ActiveCfg = Release|Any CPU
+ {E08BC25C-B39B-40F9-8114-A8D6545EE1C1}.Release|x86.Build.0 = Release|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Debug|x64.Build.0 = Debug|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Debug|x86.Build.0 = Debug|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Release|x64.ActiveCfg = Release|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Release|x64.Build.0 = Release|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Release|x86.ActiveCfg = Release|Any CPU
+ {C2D22A33-5317-47A3-B28A-E151224D3E46}.Release|x86.Build.0 = Release|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Debug|x64.Build.0 = Debug|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Debug|x86.Build.0 = Debug|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Release|x64.ActiveCfg = Release|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Release|x64.Build.0 = Release|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Release|x86.ActiveCfg = Release|Any CPU
+ {0D8E3B5F-2511-4174-8129-275500753585}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/omnisharp.json b/omnisharp.json
new file mode 100644
index 0000000..4220f19
--- /dev/null
+++ b/omnisharp.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://json.schemastore.org/omnisharp",
+ "FormattingOptions": {
+ "OrganizeImports": true,
+ "UseTabs": true,
+ "TabSize": 4,
+ "IndentationSize": 4,
+ "NewLinesForBracesInTypes": false,
+ "NewLinesForBracesInMethods": false,
+ "NewLinesForBracesInProperties": false,
+ "NewLinesForBracesInAccessors": false,
+ "NewLinesForBracesInAnonymousMethods": false,
+ "NewLinesForBracesInControlBlocks": false,
+ "NewLinesForBracesInAnonymousTypes": false,
+ "NewLinesForBracesInObjectCollectionArrayInitializers": false,
+ "NewLinesForBracesInLambdaExpressionBody": false,
+ "NewLineForElse": true,
+ "NewLineForCatch": true,
+ "NewLineForFinally": true,
+ "NewLineForMembersInObjectInit": false,
+ "NewLineForMembersInAnonymousTypes": false,
+ "NewLineForClausesInQuery": false
+ }
+}
\ No newline at end of file
diff --git a/scraper/Pipfile b/scraper/Pipfile
deleted file mode 100644
index 864234d..0000000
--- a/scraper/Pipfile
+++ /dev/null
@@ -1,14 +0,0 @@
-[[source]]
-url = "https://pypi.org/simple"
-verify_ssl = true
-name = "pypi"
-
-[packages]
-beautifulsoup4 = "*"
-requests = "*"
-pytz = "*"
-
-[dev-packages]
-
-[requires]
-python_version = "3.9"
diff --git a/scraper/Pipfile.lock b/scraper/Pipfile.lock
deleted file mode 100644
index 1bb4905..0000000
--- a/scraper/Pipfile.lock
+++ /dev/null
@@ -1,85 +0,0 @@
-{
- "_meta": {
- "hash": {
- "sha256": "d7e3ebca9807b4f0c9dcac014554e9d1c9cb3a0c30b5c71b0b7cd4ccdc4934e1"
- },
- "pipfile-spec": 6,
- "requires": {
- "python_version": "3.9"
- },
- "sources": [
- {
- "name": "pypi",
- "url": "https://pypi.org/simple",
- "verify_ssl": true
- }
- ]
- },
- "default": {
- "beautifulsoup4": {
- "hashes": [
- "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35",
- "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25",
- "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"
- ],
- "index": "pypi",
- "version": "==4.9.3"
- },
- "certifi": {
- "hashes": [
- "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
- "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
- ],
- "version": "==2021.5.30"
- },
- "charset-normalizer": {
- "hashes": [
- "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
- "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
- ],
- "markers": "python_version >= '3'",
- "version": "==2.0.4"
- },
- "idna": {
- "hashes": [
- "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
- "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
- ],
- "markers": "python_version >= '3'",
- "version": "==3.2"
- },
- "pytz": {
- "hashes": [
- "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
- "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
- ],
- "index": "pypi",
- "version": "==2021.1"
- },
- "requests": {
- "hashes": [
- "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
- "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
- ],
- "index": "pypi",
- "version": "==2.26.0"
- },
- "soupsieve": {
- "hashes": [
- "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc",
- "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"
- ],
- "markers": "python_version >= '3'",
- "version": "==2.2.1"
- },
- "urllib3": {
- "hashes": [
- "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
- "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
- ],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
- "version": "==1.26.6"
- }
- },
- "develop": {}
-}
diff --git a/scraper/__init__.py b/scraper/__init__.py
deleted file mode 100644
index 426b2a3..0000000
--- a/scraper/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__all__ = ['scraper']
diff --git a/scraper/main.py b/scraper/main.py
deleted file mode 100644
index 7de5a40..0000000
--- a/scraper/main.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from scraper import scrape
-
-_NO_DEFAULT = object()
-
-def check_yes_no(input: str, default=_NO_DEFAULT, considered_yes=None) -> bool:
- input = str(input).strip().lower()
- if not input:
- if default == _NO_DEFAULT:
- raise Exception('Empty input with no default')
- return default
- if not considered_yes:
- considered_yes = ['y', 'yes', 't', 'true', '1']
- return input in considered_yes
-
-def main():
- train_no = int(input('Train number: '))
- use_yesterday = input('Train departed yesterday? [y/N] ')
- data = scrape(train_no, use_yesterday=check_yes_no(use_yesterday, default=False))
- print(f'Train {train_no}\t{data["route"]["from"]}\t{data["route"]["to"]}')
- print()
- if 'status' in data and data['status']:
- delay = data['status']['delay']
- if delay == 0:
- delay = 'on time'
- else:
- delay = f'{delay} min'
- state = data['status']['state']
- station = data['status']['station']
- print(f'Status: {delay}\t{state}\t{station}')
- print()
- for station in data['stations']:
- if 'arrival' in station and station['arrival']:
- print(station['arrival']['scheduleTime'], end='\t')
- else:
- print(end='\t')
- print(station['name'], end='\t')
- if 'departure' in station and station['departure']:
- print(station['departure']['scheduleTime'], end='\t')
- else:
- print(end='\t')
- print()
-
-if __name__ == '__main__':
- main()
\ No newline at end of file
diff --git a/scraper/omnisharp.json b/scraper/omnisharp.json
new file mode 120000
index 0000000..4ed1f68
--- /dev/null
+++ b/scraper/omnisharp.json
@@ -0,0 +1 @@
+../omnisharp.json
\ No newline at end of file
diff --git a/scraper/schemas.py b/scraper/schemas.py
deleted file mode 100644
index 6509204..0000000
--- a/scraper/schemas.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from contextlib import ExitStack as _ExitStack
-
-_es = _ExitStack()
-
-def _load_file(name: str):
- import json
- from os.path import join, dirname
- dir = dirname(__file__)
-
- return json.load(_es.enter_context(open(join(dir, name))))
-
-TRAIN_INFO_SCHEMA = {
- 'v1': _load_file('scrape_train_schema.json'),
- 'v2': _load_file('scrape_train_schema_v2.json'),
-}
-STATION_SCHEMA = {
- 'v2': _load_file('scrape_station_schema_v2.json'),
-}
-
-_es.close()
diff --git a/scraper/scrape_station.py b/scraper/scrape_station.py
deleted file mode 100644
index 335644f..0000000
--- a/scraper/scrape_station.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import re
-
-from datetime import datetime, timedelta
-
-import pytz
-import requests
-from bs4 import BeautifulSoup
-
-from .utils import *
-
-# region regex definitions
-
-RO_LETTERS = r'A-Za-zăâîșțĂÂÎȚȘ'
-
-STATION_INFO_REGEX = re.compile(rf'^([{RO_LETTERS}.0-9 ]+) în ([0-9.]+)$')
-
-STOPPING_TIME_REGEX = re.compile(r'^(necunoscută \(stație terminus\))|(?:([0-9]+) (min|sec) \((?:începând cu|până la) ([0-9]{1,2}:[0-9]{2})\))$')
-
-# endregion
-
-def scrape(station_name: str):
- station_name = ro_letters_to_en(station_name)
- # Start scrapping session
- s = requests.Session()
-
- r = s.get(build_url(
- 'https://mersultrenurilor.infofer.ro/ro-RO/Statie/{station}',
- station=station_name.replace(' ', '-'),
- ))
-
- soup = BeautifulSoup(r.text, features='html.parser')
- sform = soup.find(id='form-search')
- result_data = { elem['name']: elem['value'] for elem in sform('input') }
-
- r = s.post('https://mersultrenurilor.infofer.ro/ro-RO/Stations/StationsResult', data=result_data)
- soup = BeautifulSoup(r.text, features='html.parser')
-
- scraped = {}
-
- station_info_div, _, departures_div, arrivals_div, *_ = soup('div', recursive=False)
-
- scraped['stationName'], scraped['date'] = STATION_INFO_REGEX.match(collapse_space(station_info_div.h2.text)).groups()
- date_d, date_m, date_y = (int(comp) for comp in scraped['date'].split('.'))
- date = datetime(date_y, date_m, date_d)
- dt_seq = DateTimeSequencer(date.year, date.month, date.day)
- tz = pytz.timezone('Europe/Bucharest')
-
- def parse_arrdep_list(elem, end_station_field_name):
- if elem.div.ul is None:
- return None
-
- def parse_item(elem):
- result = {}
-
- try:
- data_div, status_div = elem('div', recursive=False)
- except ValueError:
- data_div, *_ = elem('div', recursive=False)
- status_div = None
- data_main_div, data_details_div = data_div('div', recursive=False)
- time_div, dest_div, train_div, *_ = data_main_div('div', recursive=False)
- operator_div, route_div, stopping_time_div = data_details_div.div('div', recursive=False)
-
- result['time'] = collapse_space(time_div.div.div('div', recursive=False)[1].text)
- st_hr, st_min = (int(comp) for comp in result['time'].split(':'))
- result['time'] = tz.localize(dt_seq(st_hr, st_min)).isoformat()
-
- unknown_st, st, minsec, st_opposite_time = STOPPING_TIME_REGEX.match(
- collapse_space(stopping_time_div.div('div', recursive=False)[1].text)
- ).groups()
- if unknown_st:
- result['stoppingTime'] = None
- elif st:
- minutes = minsec == 'min'
- result['stoppingTime'] = int(st) * 60 if minutes else int(st)
-
- result['train'] = {}
- result['train']['rank'] = collapse_space(train_div.div.div('div', recursive=False)[1].span.text)
- result['train']['number'] = collapse_space(train_div.div.div('div', recursive=False)[1].a.text)
- result['train'][end_station_field_name] = collapse_space(dest_div.div.div('div', recursive=False)[1].text)
- result['train']['operator'] = collapse_space(operator_div.div('div', recursive=False)[1].text)
- result['train']['route'] = collapse_space(route_div.div('div', recursive=False)[1].text).split(' - ')
-
- return result
-
- return [parse_item(elem) for elem in elem.div.ul('li', recursive=False)]
-
- scraped['departures'] = parse_arrdep_list(departures_div, 'destination')
- scraped['arrivals'] = parse_arrdep_list(arrivals_div, 'origin')
-
- return scraped
diff --git a/scraper/scrape_station_schema_v2.json b/scraper/scrape_station_schema_v2.json
deleted file mode 100644
index a6b18f5..0000000
--- a/scraper/scrape_station_schema_v2.json
+++ /dev/null
@@ -1,138 +0,0 @@
-{
- "$schema": "http://json-schema.org/schema",
- "title": "Train Info InfoFer Scrap Station Schema",
- "description": "Results of scrapping InfoFer website for station arrival/departure info",
- "definitions": {
- "arrDepItem": {
- "type": "object",
- "properties": {
- "time": {
- "description": "Time of arrival/departure",
- "type": "string",
- "format": "date-time"
- },
- "train": {
- "type": "object",
- "properties": {
- "rank": {
- "type": "string",
- "examples": [
- "R",
- "R-E",
- "IR",
- "IRN"
- ]
- },
- "number": {
- "type": "string",
- "examples": [
- "74",
- "15934"
- ]
- },
- "operator": {
- "type": "string",
- "examples": [
- "CFR Călători",
- "Softrans",
- "Regio Călători"
- ]
- },
- "route": {
- "description": "All the stations the train stops at",
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": [
- "rank",
- "number",
- "operator"
- ]
- },
- "stoppingTime": {
- "description": "The number of seconds the train stops in the station",
- "type": [
- "integer",
- "null"
- ],
- "minimum": 0
- }
- },
- "required": [
- "time",
- "train",
- "stoppingTime"
- ]
- }
- },
- "type": "object",
- "properties": {
- "arrivals": {
- "type": ["array", "null"],
- "items": {
- "allOf": [
- {
- "$ref": "#/definitions/arrDepItem"
- },
- {
- "type": "object",
- "properties": {
- "train": {
- "type": "object",
- "properties": {
- "origin": {
- "type": "string"
- }
- },
- "required": ["origin"]
- }
- },
- "required": ["train"]
- }
- ]
- }
- },
- "departures": {
- "type": ["array", "null"],
- "items": {
- "allOf": [
- {
- "$ref": "#/definitions/arrDepItem"
- },
- {
- "type": "object",
- "properties": {
- "train": {
- "type": "object",
- "properties": {
- "destination": {
- "type": "string"
- }
- },
- "required": ["destination"]
- }
- },
- "required": ["train"]
- }
- ]
- }
- },
- "stationName": {
- "type": "string"
- },
- "date": {
- "description": "Date for which the data is provided (likely today)",
- "type": "string",
- "pattern": "^[0-9]{1,2}\\.[0-9]{2}\\.[0-9]{4}$"
- }
- },
- "required": [
- "arrivals",
- "departures",
- "stationName",
- "date"
- ]
-}
\ No newline at end of file
diff --git a/scraper/scrape_train.py b/scraper/scrape_train.py
deleted file mode 100644
index 02c9a5a..0000000
--- a/scraper/scrape_train.py
+++ /dev/null
@@ -1,146 +0,0 @@
-import re
-
-from datetime import datetime, timedelta
-
-import pytz
-import requests
-from bs4 import BeautifulSoup
-
-from .utils import *
-
-# region regex definitions
-
-TRAIN_INFO_REGEX = re.compile(r'^([A-Z-]+) ([0-9]+) în ([0-9.]+)$')
-
-OPERATOR_REGEX = re.compile(r'^Operat de (.+)$')
-
-SL_REGEX = re.compile(r'^(?:Fără|([0-9]+) min) (întârziere|mai devreme) la (trecerea fără oprire prin|sosirea în|plecarea din) (.+)\.$')
-SL_STATE_MAP = {
- 't': 'passing',
- 's': 'arrival',
- 'p': 'departure',
-}
-
-RO_LETTERS = r'A-Za-zăâîșțĂÂÎȚȘ'
-
-ROUTE_REGEX = re.compile(rf'^Parcurs tren ([{RO_LETTERS} ]+)[-–]([{RO_LETTERS} ]+)$')
-
-KM_REGEX = re.compile(r'^km ([0-9]+)$')
-
-PLATFORM_REGEX = re.compile(r'^linia (.+)$')
-
-STOPPING_TIME_REGEX = re.compile(r'^([0-9]+) (min|sec) oprire$')
-
-STATION_DEPARR_STATUS_REGEX = re.compile(r'^(?:(la timp)|(?:((?:\+|-)[0-9]+) min \((?:(?:întârziere)|(?:mai devreme))\)))(\*?)$')
-
-# endregion
-
-def scrape(train_no: str, use_yesterday=False, date_override=None):
- # Start scrapping session
- s = requests.Session()
-
- date = datetime.today()
- if use_yesterday:
- date -= timedelta(days=1)
- if date_override:
- date = date_override
-
- r = s.get(build_url(
- 'https://mersultrenurilor.infofer.ro/ro-RO/Tren/{train_no}',
- train_no=train_no,
- query=[
- ('Date', date.strftime('%d.%m.%Y')),
- ],
- ))
-
- soup = BeautifulSoup(r.text, features='html.parser')
- sform = soup.find(id='form-search')
- result_data = { elem['name']: elem['value'] for elem in sform('input') }
-
- r = s.post('https://mersultrenurilor.infofer.ro/ro-RO/Trains/TrainsResult', data=result_data)
- soup = BeautifulSoup(r.text, features='html.parser')
-
- scraped = {}
-
- train_info_div, _, _, results_div, *_ = soup('div', recursive=False)
-
- train_info_div = train_info_div.div('div', recursive=False)[0]
-
- scraped['rank'], scraped['number'], scraped['date'] = TRAIN_INFO_REGEX.match(collapse_space(train_info_div.h2.text)).groups()
- date_d, date_m, date_y = (int(comp) for comp in scraped['date'].split('.'))
- date = datetime(date_y, date_m, date_d)
-
- scraped['operator'] = OPERATOR_REGEX.match(collapse_space(train_info_div.p.text)).groups()[0]
-
- results_div = results_div.div
- status_div = results_div('div', recursive=False)[0]
- route_text = collapse_space(status_div.h4.text)
- route_from, route_to = ROUTE_REGEX.match(route_text).groups()
- scraped['route'] = {
- 'from': route_from,
- 'to': route_to,
- }
- try:
- status_line_match = SL_REGEX.match(collapse_space(status_div.div.text))
- slm_delay, slm_late, slm_arrival, slm_station = status_line_match.groups()
- scraped['status'] = {
- 'delay': (int(slm_delay) if slm_late == 'întârziere' else -int(slm_delay)) if slm_delay else 0,
- 'station': slm_station,
- 'state': SL_STATE_MAP[slm_arrival[0]],
- }
- except Exception:
- scraped['status'] = None
-
- stations = status_div.ul('li', recursive=False)
- scraped['stations'] = []
- dt_seq = DateTimeSequencer(date.year, date.month, date.day)
- tz = pytz.timezone('Europe/Bucharest')
- for station in stations:
- station_scraped = {}
-
- left, middle, right = station.div('div', recursive=False)
- station_scraped['name'] = collapse_space(middle.div.div('div', recursive=False)[0]('div', recursive=False)[0].text)
- station_scraped['km'] = collapse_space(middle.div.div('div', recursive=False)[0]('div', recursive=False)[1].text)
- station_scraped['km'] = int(KM_REGEX.match(station_scraped['km']).groups()[0])
- station_scraped['stoppingTime'] = collapse_space(middle.div.div('div', recursive=False)[0]('div', recursive=False)[2].text)
- if not station_scraped['stoppingTime']:
- station_scraped['stoppingTime'] = None
- else:
- st_value, st_minsec = STOPPING_TIME_REGEX.match(station_scraped['stoppingTime']).groups()
- station_scraped['stoppingTime'] = int(st_value)
- if st_minsec == 'min':
- station_scraped['stoppingTime'] *= 60
- station_scraped['platform'] = collapse_space(middle.div.div('div', recursive=False)[0]('div', recursive=False)[3].text)
- if not station_scraped['platform']:
- station_scraped['platform'] = None
- else:
- station_scraped['platform'] = PLATFORM_REGEX.match(station_scraped['platform']).groups()[0]
-
- def scrape_time(elem, setter):
- parts = elem.div.div('div', recursive=False)
- if parts:
- result = {}
-
- time, *_ = parts
- result['scheduleTime'] = collapse_space(time.text)
- st_hr, st_min = (int(comp) for comp in result['scheduleTime'].split(':'))
- result['scheduleTime'] = tz.localize(dt_seq(st_hr, st_min)).isoformat()
- if len(parts) >= 2:
- _, status, *_ = parts
- result['status'] = {}
- on_time, delay, approx = STATION_DEPARR_STATUS_REGEX.match(collapse_space(status.text)).groups()
- result['status']['delay'] = 0 if on_time else int(delay)
- result['status']['real'] = not approx
- else:
- result['status'] = None
-
- setter(result)
- else:
- setter(None)
-
- scrape_time(left, lambda value: station_scraped.update(arrival=value))
- scrape_time(right, lambda value: station_scraped.update(departure=value))
-
- scraped['stations'].append(station_scraped)
-
- return scraped
diff --git a/scraper/scrape_train_schema.json b/scraper/scrape_train_schema.json
deleted file mode 100644
index 2f588b6..0000000
--- a/scraper/scrape_train_schema.json
+++ /dev/null
@@ -1,134 +0,0 @@
-{
- "$schema": "http://json-schema.org/schema",
- "title": "Train Info InfoFer Scrap Train Schema",
- "description": "Results of scrapping InfoFer website for train info",
- "definitions": {
- "delayType": {
- "description": "Delay of the train (negative for being early)",
- "type": "integer"
- },
- "stationArrDepTime": {
- "description": "Time of arrival at/departure from station",
- "type": ["object", "null"],
- "properties": {
- "scheduleTime": {
- "description": "The time the train is scheduled to arrive/depart",
- "type": "string",
- "pattern": "^[0-9]{1,2}:[0-9]{2}$"
- },
- "status": {
- "type": ["object", "null"],
- "properties": {
- "delay": {
- "$ref": "#/definitions/delayType"
- },
- "real": {
- "description": "Determines whether delay was actually reported or is an approximation",
- "type": "boolean"
- }
- },
- "required": ["delay", "real"]
- }
- },
- "required": ["scheduleTime"]
- }
- },
- "type": "object",
- "properties": {
- "rank": {
- "description": "The rank of the train",
- "type": "string",
- "examples": [
- "R",
- "R-E",
- "IR",
- "IRN"
- ]
- },
- "number": {
- "description": "The number of the train",
- "type": "string",
- "examples": [
- "74",
- "15934"
- ]
- },
- "date": {
- "description": "Date of departure from the first station (dd.mm.yyyy)",
- "type": "string",
- "pattern": "^[0-9]{1,2}\\.[0-9]{2}\\.[0-9]{4}$"
- },
- "operator": {
- "description": "Operator of the train",
- "type": "string",
- "examples": [
- "CFR Călători",
- "Softrans",
- "Regio Călători"
- ]
- },
- "route": {
- "description": "Route of the train",
- "type": "object",
- "properties": {
- "from": {
- "type": "string"
- },
- "to": {
- "type": "string"
- }
- },
- "required": ["from", "to"]
- },
- "status": {
- "description": "Current status of the train",
- "type": ["object", "null"],
- "properties": {
- "delay": {
- "$ref": "#/definitions/delayType"
- },
- "station": {
- "type": "string"
- },
- "state": {
- "type": "string",
- "enum": ["passing", "arrival", "departure"]
- }
- },
- "required": ["delay", "station", "state"]
- },
- "stations": {
- "description": "List of stations the train stops at",
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "km": {
- "description": "The distance the train travelled until reaching this station",
- "type": "integer"
- },
- "stoppingTime": {
- "description": "The number of minutes the train is scheduled to stop in this station",
- "type": ["integer", "null"],
- "minimum": 0
- },
- "platform": {
- "description": "The platform the train stopped at",
- "type": ["string", "null"]
- },
- "arrival": {
- "$ref": "#/definitions/stationArrDepTime"
- },
- "departure": {
- "$ref": "#/definitions/stationArrDepTime"
- }
- },
- "required": ["name", "km"]
- }
- }
- },
- "required": ["route", "stations", "rank", "number", "date", "operator"]
-}
\ No newline at end of file
diff --git a/scraper/scrape_train_schema_v2.json b/scraper/scrape_train_schema_v2.json
deleted file mode 100644
index 4a1433a..0000000
--- a/scraper/scrape_train_schema_v2.json
+++ /dev/null
@@ -1,134 +0,0 @@
-{
- "$schema": "http://json-schema.org/schema",
- "title": "Train Info InfoFer Scrap Train Schema",
- "description": "Results of scrapping InfoFer website for train info",
- "definitions": {
- "delayType": {
- "description": "Delay of the train (negative for being early)",
- "type": "integer"
- },
- "stationArrDepTime": {
- "description": "Time of arrival at/departure from station",
- "type": ["object", "null"],
- "properties": {
- "scheduleTime": {
- "description": "The time the train is scheduled to arrive/depart",
- "type": "string",
- "format": "date-time"
- },
- "status": {
- "type": ["object", "null"],
- "properties": {
- "delay": {
- "$ref": "#/definitions/delayType"
- },
- "real": {
- "description": "Determines whether delay was actually reported or is an approximation",
- "type": "boolean"
- }
- },
- "required": ["delay", "real"]
- }
- },
- "required": ["scheduleTime"]
- }
- },
- "type": "object",
- "properties": {
- "rank": {
- "description": "The rank of the train",
- "type": "string",
- "examples": [
- "R",
- "R-E",
- "IR",
- "IRN"
- ]
- },
- "number": {
- "description": "The number of the train",
- "type": "string",
- "examples": [
- "74",
- "15934"
- ]
- },
- "date": {
- "description": "Date of departure from the first station (dd.mm.yyyy)",
- "type": "string",
- "pattern": "^[0-9]{1,2}\\.[0-9]{2}\\.[0-9]{4}$"
- },
- "operator": {
- "description": "Operator of the train",
- "type": "string",
- "examples": [
- "CFR Călători",
- "Softrans",
- "Regio Călători"
- ]
- },
- "route": {
- "description": "Route of the train",
- "type": "object",
- "properties": {
- "from": {
- "type": "string"
- },
- "to": {
- "type": "string"
- }
- },
- "required": ["from", "to"]
- },
- "status": {
- "description": "Current status of the train",
- "type": ["object", "null"],
- "properties": {
- "delay": {
- "$ref": "#/definitions/delayType"
- },
- "station": {
- "type": "string"
- },
- "state": {
- "type": "string",
- "enum": ["passing", "arrival", "departure"]
- }
- },
- "required": ["delay", "station", "state"]
- },
- "stations": {
- "description": "List of stations the train stops at",
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "km": {
- "description": "The distance the train travelled until reaching this station",
- "type": "integer"
- },
- "stoppingTime": {
- "description": "The number of seconds the train is scheduled to stop in this station",
- "type": ["integer", "null"],
- "minimum": 1
- },
- "platform": {
- "description": "The platform the train stopped at",
- "type": ["string", "null"]
- },
- "arrival": {
- "$ref": "#/definitions/stationArrDepTime"
- },
- "departure": {
- "$ref": "#/definitions/stationArrDepTime"
- }
- },
- "required": ["name", "km"]
- }
- }
- },
- "required": ["route", "stations", "rank", "number", "date", "operator"]
-}
\ No newline at end of file
diff --git a/scraper/scraper.csproj b/scraper/scraper.csproj
new file mode 100644
index 0000000..2a103a0
--- /dev/null
+++ b/scraper/scraper.csproj
@@ -0,0 +1,15 @@
+
+
+
+ enable
+ net6.0
+
+
+
+
+
+
+
+
+
+
diff --git a/scraper/scraper.py b/scraper/scraper.py
deleted file mode 100644
index 8a594d9..0000000
--- a/scraper/scraper.py
+++ /dev/null
@@ -1,12 +0,0 @@
-#! /usr/bin/env python3
-from .scrape_train import scrape as scrape_train
-from .scrape_station import scrape as scrape_station
-
-def main():
- train_no = 1538
- print(f'Testing package with train number {train_no}')
- from pprint import pprint
- pprint(scrape_train(train_no))
-
-if __name__ == '__main__':
- main()
diff --git a/scraper/setup.py b/scraper/setup.py
deleted file mode 100644
index ee96682..0000000
--- a/scraper/setup.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from distutils.core import setup
-
-setup(
- name='InfoFer_Scraper',
- version='0.1',
- author='Dan Cojocaru',
- install_requires=['beautifulsoup4', 'requests', 'pytz']
-)
\ No newline at end of file
diff --git a/scraper/src/Exceptions/TrainNotThisDayException.cs b/scraper/src/Exceptions/TrainNotThisDayException.cs
new file mode 100644
index 0000000..5fe1e06
--- /dev/null
+++ b/scraper/src/Exceptions/TrainNotThisDayException.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Runtime.Serialization;
+using JetBrains.Annotations;
+
+namespace scraper.Exceptions {
+ ///
+ /// The train that the information was requested for might be running,
+ /// but it is not running on the requested day.
+ ///
+ public class TrainNotThisDayException : Exception {
+ public TrainNotThisDayException() : base() { }
+ protected TrainNotThisDayException([NotNull] SerializationInfo info, StreamingContext context) : base(info, context) { }
+ public TrainNotThisDayException([CanBeNull] string? message) : base(message) { }
+ public TrainNotThisDayException([CanBeNull] string? message, [CanBeNull] Exception? innerException) : base(message, innerException) { }
+ }
+}
\ No newline at end of file
diff --git a/scraper/src/Models/Station.cs b/scraper/src/Models/Station.cs
new file mode 100644
index 0000000..7752742
--- /dev/null
+++ b/scraper/src/Models/Station.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using InfoferScraper.Models.Status;
+
+namespace InfoferScraper.Models.Station {
+ #region Interfaces
+
+ public interface IStationScrapeResult {
+ public string StationName { get; }
+ ///
+ /// Date in the DD.MM.YYYY format
+ /// This date is taken as-is from the result.
+ ///
+ public string Date { get; }
+
+ public IReadOnlyList? Arrivals { get; }
+ public IReadOnlyList? Departures { get; }
+ }
+
+ public interface IStationArrDep {
+ public int? StoppingTime { get; }
+ public DateTimeOffset Time { get; }
+ public IStationTrain Train { get; }
+ public IStationStatus Status { get; }
+ }
+
+ public interface IStationTrain {
+ public string Number { get; }
+ public string Operator { get; }
+ public string Rank { get; }
+ public IReadOnlyList Route { get; }
+ ///
+ /// Arrivals -> Departure station; Departures -> Destination station
+ ///
+ public string Terminus { get; }
+ }
+
+ public interface IStationStatus : IStatus {
+ new int Delay { get; }
+ new bool Real { get; }
+ public string? Platform { get; }
+ }
+
+ #endregion
+
+ #region Implementations
+
+ internal record StationScrapeResult : IStationScrapeResult {
+ private List? _modifyableArrivals = new();
+ private List? _modifyableDepartures = new();
+
+ public string StationName { get; internal set; } = "";
+ public string Date { get; internal set; } = "";
+ public IReadOnlyList? Arrivals => _modifyableArrivals?.AsReadOnly();
+ public IReadOnlyList? Departures => _modifyableDepartures?.AsReadOnly();
+
+ private void AddStationArrival(StationArrDep arrival) {
+ _modifyableArrivals ??= new List();
+ _modifyableArrivals.Add(arrival);
+ }
+
+ private void AddStationDeparture(StationArrDep departure) {
+ _modifyableDepartures ??= new List();
+ _modifyableDepartures.Add(departure);
+ }
+
+ internal void AddNewStationArrival(Action configurator) {
+ StationArrDep newStationArrDep = new();
+ configurator(newStationArrDep);
+ AddStationArrival(newStationArrDep);
+ }
+
+ internal void AddNewStationDeparture(Action configurator) {
+ StationArrDep newStationArrDep = new();
+ configurator(newStationArrDep);
+ AddStationDeparture(newStationArrDep);
+ }
+ }
+
+ internal record StationArrDep : IStationArrDep {
+ public int? StoppingTime { get; internal set; }
+ public DateTimeOffset Time { get; internal set; }
+ public IStationTrain Train => ModifyableTrain;
+ public IStationStatus Status => ModifyableStatus;
+
+ internal readonly StationTrain ModifyableTrain = new();
+ internal readonly StationStatus ModifyableStatus = new();
+ }
+
+ internal record StationTrain : IStationTrain {
+ private readonly List _modifyableRoute = new();
+
+ public string Number { get; internal set; } = "";
+ public string Operator { get; internal set; } = "";
+ public string Rank { get; internal set; } = "";
+ public IReadOnlyList Route => _modifyableRoute.AsReadOnly();
+ public string Terminus { get; internal set; } = "";
+
+ internal void AddRouteStation(string station) => _modifyableRoute.Add(station);
+ }
+
+ internal record StationStatus : IStationStatus {
+ public int Delay { get; internal set; }
+ public bool Real { get; internal set; }
+ public string? Platform { get; internal set; }
+ }
+
+ #endregion
+}
diff --git a/scraper/src/Models/Status.cs b/scraper/src/Models/Status.cs
new file mode 100644
index 0000000..0b91f64
--- /dev/null
+++ b/scraper/src/Models/Status.cs
@@ -0,0 +1,15 @@
+namespace InfoferScraper.Models.Status {
+ public interface IStatus {
+ public int Delay { get; }
+
+ ///
+ /// Determines whether delay was actually reported or is an approximation
+ ///
+ public bool Real { get; }
+ }
+
+ internal record Status : IStatus {
+ public int Delay { get; set; }
+ public bool Real { get; set; }
+ }
+}
diff --git a/scraper/src/Models/Train.cs b/scraper/src/Models/Train.cs
new file mode 100644
index 0000000..9ebdeb7
--- /dev/null
+++ b/scraper/src/Models/Train.cs
@@ -0,0 +1,316 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using InfoferScraper.Models.Status;
+using InfoferScraper.Models.Train.JsonConverters;
+
+namespace InfoferScraper.Models.Train {
+ #region Interfaces
+
+ public interface ITrainScrapeResult {
+ public string Rank { get; }
+
+ public string Number { get; }
+
+ ///
+ /// Date in the DD.MM.YYYY format
+ /// This date is taken as-is from the result.
+ ///
+ public string Date { get; }
+
+ public string Operator { get; }
+
+ public IReadOnlyList Groups { get; }
+ }
+
+ public interface ITrainGroup {
+ public ITrainRoute Route { get; }
+
+ public ITrainStatus? Status { get; }
+ public IReadOnlyList Stations { get; }
+ }
+
+ public interface ITrainRoute {
+ public string From { get; }
+ public string To { get; }
+ }
+
+ public interface ITrainStatus {
+ public int Delay { get; }
+ public string Station { get; }
+ public StatusKind State { get; }
+ }
+
+ public interface ITrainStopDescription {
+ public string Name { get; }
+ public int Km { get; }
+
+ ///
+ /// The time the train waits in the station in seconds
+ ///
+ public int? StoppingTime { get; }
+
+ public string? Platform { get; }
+ public ITrainStopArrDep? Arrival { get; }
+ public ITrainStopArrDep? Departure { get; }
+
+ public IReadOnlyList