diff --git a/.env.example b/.env.example index 5e1f7c453..b8d39192c 100644 --- a/.env.example +++ b/.env.example @@ -69,3 +69,8 @@ DRIVEBC_WEATHER_API_TOKEN_URL= DRIVEBC_WEATHER_AREAS_API_BASE_URL= WEATHER_CLIENT_ID= WEATHER_CLIENT_SECRET= + +# TESTS +# optional: set to config.test.DbcRunner to user test runner allowing for +# skipping db creation entirely +TEST_RUNNER=config.test.DbcRunner \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9dad6626b..d9da5354e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ x-function: &backend volumes: - ./src/backend:/app/backend + - ./src/images/webcams:/app/images/webcams stdin_open: true tty: true diff --git a/src/backend/apps/cms/models.py b/src/backend/apps/cms/models.py index d07c3a295..9c10a0ed6 100644 --- a/src/backend/apps/cms/models.py +++ b/src/backend/apps/cms/models.py @@ -58,7 +58,7 @@ class Bulletin(Page, BaseModel): page_body = "Use this page for creating bulletins." teaser = models.CharField(max_length=250, blank=True) body = RichTextField() - image = models.ForeignKey(Image, on_delete=models.CASCADE, null=True, blank=False) + image = models.ForeignKey(Image, on_delete=models.SET_NULL, null=True, blank=False) image_alt_text = models.CharField(max_length=125, default='', blank=False) def rendered_body(self): @@ -93,7 +93,7 @@ class Ferry(Page, BaseModel): location = models.GeometryField(blank=True, null=True) url = models.URLField(blank=True) - image = models.ForeignKey(Image, on_delete=models.CASCADE, blank=True, null=True) + image = models.ForeignKey(Image, on_delete=models.SET_NULL, blank=True, null=True) description = RichTextField(max_length=750, blank=True) seasonal_description = RichTextField(max_length=100, blank=True) diff --git a/src/backend/apps/event/models.py b/src/backend/apps/event/models.py index 523a74e09..faf632dd4 100644 --- a/src/backend/apps/event/models.py +++ b/src/backend/apps/event/models.py @@ -34,7 +34,7 @@ class Event(BaseModel): last_updated = models.DateTimeField() # Schedule - schedule = models.JSONField(default={}) + schedule = models.JSONField(default=dict) # Scheduled start and end start = models.DateTimeField(null=True) diff --git a/src/backend/apps/feed/serializers.py b/src/backend/apps/feed/serializers.py index 6b859de06..9cc8e5362 100644 --- a/src/backend/apps/feed/serializers.py +++ b/src/backend/apps/feed/serializers.py @@ -27,6 +27,8 @@ class WebcamFeedSerializer(serializers.Serializer): # Description camName = DriveBCField('name', source="*") caption = serializers.CharField(max_length=256, allow_blank=True, allow_null=True) + dbcMark = DriveBCField('dbc_mark', source="*") + credit = DriveBCField('credit', source="*") # Location region = WebcamRegionField(source="*") diff --git a/src/backend/apps/feed/tasks.py b/src/backend/apps/feed/tasks.py index a11cc38c6..562a3a54e 100644 --- a/src/backend/apps/feed/tasks.py +++ b/src/backend/apps/feed/tasks.py @@ -31,6 +31,7 @@ def populate_ferry_task(): def publish_scheduled(): call_command('publish_scheduled') + @db_periodic_task(crontab(minute="*/5")) def populate_regional_weather_task(): populate_all_regional_weather_data() \ No newline at end of file diff --git a/src/backend/apps/shared/tests.py b/src/backend/apps/shared/tests.py index b3a4d6cf4..1fe78218f 100644 --- a/src/backend/apps/shared/tests.py +++ b/src/backend/apps/shared/tests.py @@ -11,7 +11,7 @@ from rest_framework.test import APIRequestFactory -from src.backend.apps.shared.views import FeedbackView +from apps.shared.views import FeedbackView logger = logging.getLogger(__name__) @@ -52,4 +52,4 @@ def tearDown(self): Event.objects.all().delete() Ferry.objects.all().delete() RegionalWeather.objects.all().delete() - + diff --git a/src/backend/apps/weather/tests/test_regional_weather_populate.py b/src/backend/apps/weather/tests/test_regional_weather_populate.py index 7ee526ac3..073239e06 100644 --- a/src/backend/apps/weather/tests/test_regional_weather_populate.py +++ b/src/backend/apps/weather/tests/test_regional_weather_populate.py @@ -10,7 +10,7 @@ populate_all_regional_weather_data, ) from apps.weather.tests.test_data.regional_weather_parsed_feed import json_feed -from src.backend.apps.feed.client import FeedClient +from apps.feed.client import FeedClient from unittest import mock class TestRegionalWeatherModel(BaseTest): @@ -36,7 +36,7 @@ def test_populate_regional_weather_function(self): regional_weather_one = RegionalWeather.objects.get(code="s0000341") assert regional_weather_one.location_latitude == \ "58.66N" - + def test_populate_regional_weather_function_with_existing_data(self): RegionalWeather.objects.create( code="s0000341", @@ -48,7 +48,7 @@ def test_populate_regional_weather_function_with_existing_data(self): assert regional_weather_one.location_latitude == \ "58.66N" - @patch('src.backend.apps.feed.client.FeedClient.get_regional_weather_list') + @patch('apps.feed.client.FeedClient.get_regional_weather_list') def test_populate_and_update_regional_weather(self, mock_requests_get): mock_requests_get.side_effect = [ MockResponse(self.mock_regional_weather_feed_result, status_code=200), @@ -62,7 +62,7 @@ def test_populate_and_update_regional_weather(self, mock_requests_get): populate_regional_weather_from_data(regional_weather_data) weather_list_length = len(response) assert weather_list_length == 2 - + def test_populate_all_regional_weather_data(self): with mock.patch('requests.post') as mock_post, mock.patch('requests.get') as mock_get: # Mock the response for requests.post @@ -80,6 +80,6 @@ def test_populate_all_regional_weather_data(self): # Mock the response for the second requests.get (weather data for a specific area codes) mock.Mock(json=lambda: self.mock_regional_weather_feed_result_weather_one), ] - + populate_all_regional_weather_data() assert len(RegionalWeather.objects.all()) == 1 \ No newline at end of file diff --git a/src/backend/apps/webcam/management/commands/update_webcam.py b/src/backend/apps/webcam/management/commands/update_webcam.py new file mode 100644 index 000000000..7ce9ffcaa --- /dev/null +++ b/src/backend/apps/webcam/management/commands/update_webcam.py @@ -0,0 +1,13 @@ +from apps.webcam.tasks import update_single_webcam_data +from django.core.management.base import BaseCommand + +from apps.webcam.models import Webcam + +class Command(BaseCommand): + + def add_arguments(self , parser): + parser.add_argument('id' , nargs='?' , type=int) + + def handle(self, id, *args, **options): + cam = Webcam.objects.get(id=id) + update_single_webcam_data(cam) diff --git a/src/backend/apps/webcam/static/BCSans.otf b/src/backend/apps/webcam/static/BCSans.otf new file mode 100644 index 000000000..bea110185 Binary files /dev/null and b/src/backend/apps/webcam/static/BCSans.otf differ diff --git a/src/backend/apps/webcam/tasks.py b/src/backend/apps/webcam/tasks.py index 51e4b834f..b3a0b9305 100644 --- a/src/backend/apps/webcam/tasks.py +++ b/src/backend/apps/webcam/tasks.py @@ -1,7 +1,20 @@ import datetime +import io import logging +from math import floor +import os +from pprint import pprint +import re +import urllib.request +from zoneinfo import ZoneInfo + +from django.conf import settings +from PIL import Image, ImageDraw, ImageFont +from PIL.ExifTags import TAGS +from pathlib import Path import pytz + from apps.feed.client import FeedClient from apps.webcam.enums import CAMERA_DIFF_FIELDS from apps.webcam.models import Webcam @@ -11,6 +24,10 @@ logger = logging.getLogger(__name__) +APP_DIR = Path(__file__).resolve().parent +FONT = ImageFont.truetype(f'{APP_DIR}/static/BCSans.otf', size=14) +CAMS_DIR = f'{settings.SRC_DIR}/images/webcams' + def populate_webcam_from_data(webcam_data): webcam_id = webcam_data.get("id") @@ -44,6 +61,7 @@ def update_single_webcam_data(webcam): webcam_serializer = WebcamSerializer(webcam, data=webcam_data) webcam_serializer.is_valid(raise_exception=True) webcam_serializer.save() + update_webcam_image(webcam_data) return @@ -55,3 +73,70 @@ def update_all_webcam_data(): # Rebuild cache WebcamAPI().set_list_data() + + +def update_webcam_image(webcam): + ''' + Retrieve the current cam image, stamp it and save it + + Per JIRA ticket DBC22-1857 + + ''' + + try: + # retrieve source image into a file-like buffer + base_url = settings.DRIVEBC_WEBCAM_API_BASE_URL + if base_url[-1] == '/': + base_url = base_url[:-1] + endpoint = f'{base_url}/webcams/{webcam["id"]}/imageSource' + + with urllib.request.urlopen(endpoint) as url: + image_data = io.BytesIO(url.read()) + + raw = Image.open(image_data) + width, height = raw.size + if width > 800: + ratio = 800 / width + width = 800 + height = floor(height * ratio) + raw = raw.resize((width, height)) + + stamped = Image.new('RGB', (width, height + 18)) + stamped.paste(raw) # leaves 18 pixel black bar left at bottom + + # add mark and timestamp to black bar + pen = ImageDraw.Draw(stamped) + mark = webcam.get('dbc_mark', '') + pen.text((3, height + 14), mark, fill="white", anchor='ls', font=FONT) + + lastmod = webcam.get('last_update_modified') + timestamp = 'Last modification time unavailable' + if lastmod is not None: + month = lastmod.strftime('%b') + day = lastmod.strftime('%d') + day = day[1:] if day[:1] == '0' else day # strip leading zero + timestamp = f'{month} {day}, {lastmod.strftime("%Y %H:%M:%S %p %Z")}' + pen.text((width - 3, height + 14), timestamp, fill="white", anchor='rs', font=FONT) + + # save image in shared volume + filename = f'{CAMS_DIR}/{webcam["id"]}.jpg' + with open(filename, 'wb') as saved: + stamped.save(saved, 'jpeg', quality=95, exif=raw.getexif()) + + # Set the last modified time to the last modified time plus a timedelta + # calculated as 110% of the mean time between updates, minus the standard + # deviation. If that can't be calculated, default to 5 minutes. This is + # then used to set the expires header in nginx. + delta = 300 # 5 minutes + try: + mean = webcam.get('update_period_mean') + stddev = webcam.get('update_period_stddev', 0) + delta = (1.1 * mean) - stddev + except: + pass # update period times not available + delta = datetime.timedelta(seconds=delta) + if lastmod is not None: + lastmod = floor((lastmod + delta).timestamp()) # POSIX timestamp + os.utime(filename, times=(lastmod, lastmod)) + except Exception as e: + logger.error(e) diff --git a/src/backend/apps/webcam/tests/228.jpg b/src/backend/apps/webcam/tests/228.jpg new file mode 100644 index 000000000..470d78d20 Binary files /dev/null and b/src/backend/apps/webcam/tests/228.jpg differ diff --git a/src/backend/apps/webcam/tests/test_webcam_update_images.py b/src/backend/apps/webcam/tests/test_webcam_update_images.py new file mode 100644 index 000000000..70b27bac0 --- /dev/null +++ b/src/backend/apps/webcam/tests/test_webcam_update_images.py @@ -0,0 +1,20 @@ +import datetime +import json +import zoneinfo +from pathlib import Path +from unittest.mock import patch + +from apps.shared import enums as shared_enums +from apps.shared.enums import CacheKey +from apps.shared.tests import BaseTest, MockResponse +from apps.webcam.models import Webcam +from apps.webcam.views import WebcamAPI +from django.contrib.gis.geos import Point +from django.core.cache import cache +from rest_framework.test import APITestCase + + +class TestWebcamUpdateImages(BaseTest): + + def test_webcam_image_update(self): + pass diff --git a/src/backend/config/settings/django.py b/src/backend/config/settings/django.py index 91afdc737..77d77981e 100644 --- a/src/backend/config/settings/django.py +++ b/src/backend/config/settings/django.py @@ -179,3 +179,5 @@ "level": "WARNING", }, } + +TEST_RUNNER = env("TEST_RUNNER", default="django.test.runner.DiscoverRunner") diff --git a/src/backend/config/settings/third_party.py b/src/backend/config/settings/third_party.py index 034c156f5..d4da6a81b 100644 --- a/src/backend/config/settings/third_party.py +++ b/src/backend/config/settings/third_party.py @@ -1,3 +1,4 @@ +import os from pathlib import Path import environ @@ -32,6 +33,12 @@ # Wagtail WAGTAIL_SITE_NAME = 'DriveBC' WAGTAILEMBEDS_RESPONSIVE_HTML = True +WAGTAILADMIN_BASE_URL = "\\cms-admin" # reCAPTCHA DRF_RECAPTCHA_SECRET_KEY = env("DJANGO_RECAPTCHA_SECRET_KEY") + +# On windows, GDAL and GEOS require explicit paths to the dlls +if os.name == "nt": + GEOS_LIBRARY_PATH = env("GEOS_LIBRARY_PATH") + GDAL_LIBRARY_PATH = env("GDAL_LIBRARY_PATH") diff --git a/src/backend/config/test.py b/src/backend/config/test.py new file mode 100644 index 000000000..c2bece9d7 --- /dev/null +++ b/src/backend/config/test.py @@ -0,0 +1,28 @@ +from argparse import ArgumentParser +from typing import Any +from django.db.backends.base.base import BaseDatabaseWrapper +from django.test.runner import DiscoverRunner + + +class DbcRunner(DiscoverRunner): + '''Custom test runner to allow for skipping database creation altogether.''' + + def __init__(self, *args, **kwargs): + self.skipdb = kwargs["skipdb"] + super().__init__(*args, **kwargs) + + @classmethod + def add_arguments(cls, parser: ArgumentParser) -> None: + parser.add_argument("--skipdb", action="store_true", default=False, + help="Don't create a test database") + DiscoverRunner.add_arguments(parser) + + def setup_databases(self, **kwargs: Any) -> list[tuple[BaseDatabaseWrapper, str, bool]]: + if self.skipdb: + return + return super().setup_databases(**kwargs) + + def teardown_databases(self, old_config: list[tuple[BaseDatabaseWrapper, str, bool]], **kwargs: Any) -> None: + if self.skipdb: + return + return super().teardown_databases(old_config, **kwargs) diff --git a/src/backend/test.py b/src/backend/test.py new file mode 100644 index 000000000..754a02647 --- /dev/null +++ b/src/backend/test.py @@ -0,0 +1,1088 @@ +from datetime import datetime +from zoneinfo import ZoneInfo +from pprint import pprint +import json +import re +import requests + +import pytz + +NEWLINES = re.compile(r'\n') +HTML = re.compile('<.*?>') + +from PIL import Image, ImageDraw, ImageFont + +font = ImageFont.truetype('BCSans.otf', size=16) + +im = Image.open('228.jpg') +tz = ZoneInfo("America/Vancouver") + +x, y = im.size +ti = Image.new('RGB', (x, y + 24)) +ti.paste(im) +d = ImageDraw.Draw(ti) +now = datetime.now(tz=tz) +month = now.strftime('%b') +day = now.strftime('%d') +day = day[1:] if day[:1] == '0' else day +ts = f'{month} {day}, {now.strftime("%Y %H:%M:%S %p %Z")}' +d.text((x - 5, y + 18), ts, fill="white", anchor='rs', font=font) +d.text((5, y + 18), "DriveBC.ca", fill="white", anchor='ls', font=font) +# ti.show() + +ids = [ + 893, + 126, + 478, + 254, + 495, + 156, + 874, + 198, + 172, + 197, + 134, + 601, + 602, + 566, + 593, + 146, + 892, + 111, + 1039, + 286, + 794, + 143, + 795, + 378, + 775, + 776, + 778, + 336, + 799, + 823, + 859, + 800, + 6, + 703, + 702, + 701, + 779, + 781, + 782, + 500, + 77, + 815, + 816, + 817, + 812, + 813, + 367, + 784, + 785, + 786, + 236, + 496, + 828, + 122, + 801, + 802, + 803, + 774, + 772, + 773, + 936, + 787, + 788, + 789, + 808, + 809, + 97, + 1017, + 966, + 98, + 99, + 217, + 968, + 967, + 216, + 864, + 865, + 339, + 483, + 141, + 560, + 804, + 807, + 989, + 990, + 226, + 805, + 987, + 988, + 123, + 887, + 205, + 850, + 943, + 1083, + 140, + 353, + 489, + 151, + 256, + 1040, + 1041, + 144, + 110, + 255, + 945, + 484, + 485, + 124, + 970, + 612, + 125, + 980, + 589, + 590, + 591, + 592, + 819, + 820, + 821, + 822, + 288, + 10, + 979, + 471, + 472, + 473, + 475, + 253, + 488, + 888, + 889, + 886, + 166, + 880, + 165, + 376, + 885, + 900, + 100, + 870, + 871, + 924, + 940, + 925, + 958, + 960, + 957, + 959, + 565, + 382, + 200, + 335, + 491, + 328, + 840, + 466, + 912, + 913, + 298, + 297, + 109, + 377, + 1103, + 1104, + 588, + 155, + 451, + 594, + 595, + 239, + 1059, + 295, + 294, + 877, + 878, + 1102, + 1095, + 1094, + 337, + 1098, + 1099, + 1101, + 1100, + 842, + 777, + 59, + 431, + 604, + 603, + 673, + 672, + 670, + 671, + 736, + 12, + 517, + 296, + 697, + 698, + 668, + 669, + 584, + 585, + 586, + 587, + 11, + 486, + 159, + 101, + 487, + 386, + 385, + 875, + 876, + 135, + 901, + 902, + 214, + 189, + 142, + 1016, + 218, + 219, + 558, + 557, + 524, + 220, + 221, + 383, + 467, + 1112, + 65, + 1064, + 1065, + 246, + 452, + 862, + 863, + 235, + 810, + 941, + 942, + 64, + 446, + 265, + 266, + 783, + 247, + 157, + 174, + 162, + 334, + 793, + 158, + 5, + 306, + 754, + 1004, + 1005, + 129, + 881, + 882, + 631, + 213, + 1090, + 1091, + 928, + 929, + 930, + 60, + 175, + 131, + 1116, + 1117, + 1118, + 569, + 567, + 570, + 1072, + 1074, + 1071, + 303, + 1030, + 304, + 1031, + 632, + 935, + 965, + 470, + 160, + 1025, + 1026, + 307, + 61, + 1056, + 1057, + 1055, + 1058, + 56, + 287, + 58, + 818, + 657, + 687, + 688, + 161, + 798, + 685, + 686, + 379, + 380, + 676, + 677, + 2, + 734, + 674, + 675, + 666, + 667, + 281, + 282, + 1044, + 1045, + 501, + 502, + 1110, + 1111, + 453, + 338, + 605, + 606, + 607, + 608, + 972, + 971, + 250, + 1028, + 1043, + 1027, + 442, + 860, + 861, + 443, + 1053, + 1054, + 755, + 1049, + 1048, + 1050, + 229, + 173, + 494, + 627, + 628, + 630, + 879, + 305, + 759, + 626, + 455, + 469, + 139, + 952, + 506, + 248, + 1023, + 1024, + 518, + 188, + 969, + 482, + 481, + 164, + 890, + 355, + 289, + 760, + 761, + 762, + 234, + 633, + 583, + 249, + 561, + 914, + 937, + 938, + 939, + 183, + 132, + 1021, + 1022, + 883, + 884, + 133, + 177, + 1002, + 1003, + 176, + 130, + 252, + 866, + 1076, + 769, + 770, + 771, + 55, + 872, + 340, + 873, + 1051, + 1052, + 78, + 516, + 53, + 368, + 922, + 923, + 230, + 57, + 532, + 533, + 127, + 147, + 148, + 149, + 150, + 637, + 638, + 639, + 640, + 369, + 634, + 635, + 636, + 215, + 454, + 891, + 370, + 867, + 868, + 41, + 194, + 251, + 497, + 96, + 206, + 1070, + 228, + 707, + 662, + 663, + 622, + 623, + 624, + 625, + 618, + 619, + 620, + 621, + 614, + 615, + 616, + 617, + 1006, + 1007, + 853, + 1069, + 1068, + 854, + 852, + 851, + 763, + 73, + 72, + 71, + 1029, + 36, + 113, + 112, + 115, + 114, + 683, + 684, + 292, + 275, + 903, + 904, + 905, + 906, + 277, + 278, + 279, + 280, + 535, + 536, + 537, + 538, + 54, + 354, + 356, + 461, + 462, + 463, + 464, + 268, + 271, + 269, + 270, + 204, + 223, + 224, + 225, + 405, + 403, + 404, + 406, + 656, + 7, + 1096, + 1097, + 556, + 559, + 128, + 629, + 468, + 330, + 333, + 331, + 332, + 324, + 327, + 325, + 326, + 806, + 240, + 899, + 898, + 918, + 919, + 920, + 921, + 231, + 193, + 195, + 192, + 362, + 360, + 359, + 361, + 364, + 365, + 366, + 363, + 1012, + 1013, + 1014, + 1015, + 998, + 999, + 1008, + 1009, + 1011, + 1010, + 459, + 456, + 457, + 458, + 449, + 450, + 448, + 460, + 954, + 961, + 503, + 504, + 232, + 505, + 493, + 492, + 465, + 242, + 241, + 726, + 725, + 719, + 720, + 827, + 826, + 358, + 357, + 721, + 722, + 723, + 724, + 995, + 996, + 997, + 199, + 973, + 841, + 329, + 916, + 917, + 104, + 103, + 79, + 82, + 80, + 81, + 708, + 709, + 710, + 711, + 434, + 435, + 436, + 437, + 438, + 439, + 440, + 441, + 301, + 300, + 299, + 302, + 843, + 844, + 845, + 846, + 983, + 984, + 985, + 986, + 1106, + 1107, + 1108, + 1109, + 86, + 95, + 93, + 94, + 83, + 764, + 84, + 85, + 991, + 992, + 993, + 994, + 811, + 848, + 849, + 814, + 525, + 515, + 682, + 712, + 713, + 714, + 715, + 716, + 717, + 718, + 529, + 699, + 700, + 534, + 613, + 571, + 572, + 573, + 574, + 575, + 609, + 527, + 526, + 610, + 580, + 581, + 582, + 659, + 658, + 660, + 661, + 727, + 728, + 729, + 730, + 311, + 312, + 90, + 89, + 87, + 88, + 319, + 318, + 732, + 796, + 797, + 696, + 695, + 731, + 735, + 780, + 767, + 768, + 791, + 91, + 92, + 577, + 578, + 579, + 733, + 323, + 322, + 211, + 210, + 209, + 208, + 315, + 317, + 316, + 178, + 704, + 705, + 706, + 693, + 694, + 691, + 692, + 689, + 690, + 179, + 665, + 1062, + 1063, + 521, + 520, + 180, + 765, + 766, + 182, + 596, + 597, + 598, + 599, + 152, + 894, + 895, + 896, + 897, + 833, + 834, + 847, + 835, + 519, + 528, + 522, + 829, + 830, + 831, + 832, + 1020, + 1019, + 1018, + 22, + 21, + 20, + 18, + 17, + 19, + 67, + 70, + 309, + 310, + 66, + 291, + 308, + 34, + 33, + 373, + 374, + 371, + 372, + 31, + 30, + 29, + 1115, + 32, + 824, + 825, + 963, + 964, + 962, + 313, + 314, + 391, + 393, + 390, + 392, + 285, + 284, + 836, + 838, + 839, + 837, + 678, + 679, + 680, + 681, + 258, + 1000, + 1001, + 245, + 244, + 243, + 1113, + 1114, + 944, + 907, + 908, + 741, + 742, + 743, + 744, + 737, + 738, + 739, + 740, + 476, + 479, + 430, + 480, + 749, + 750, + 751, + 752, + 399, + 398, + 400, + 343, + 395, + 397, + 396, + 425, + 428, + 664, + 137, + 424, + 427, + 8, + 272, + 273, + 910, + 909, + 926, + 927, + 974, + 975, + 756, + 790, + 792, + 432, + 433, + 976, + 977, + 978, + 423, + 429, + 344, + 347, + 346, + 345, + 352, + 349, + 350, + 351, + 931, + 932, + 933, + 934, + 981, + 982, + 381, + 1084, + 1085, + 375, + 1066, + 1067, + 102, + 649, + 650, + 651, + 342, + 227, + 915, + 1087, + 1088, + 1089, + 1092, + 1093, + 611, + 757, + 758, + 568, + 1119, + 1120, + 1121, + 548, + 550, + 551, + 549, + 263, + 264, + 552, + 554, + 555, + 553, + 267, + 1042, + 238, + 652, + 653, + 654, + 655, + 855, + 856, + 857, + 858, + 507, + 510, + 509, + 508, + 511, + 514, + 513, + 512, + 498, + 499, + 48, + 190, + 531, + 402, + 955, + 956, + 1077, + 1078, + 257, + 426, + 202, + 1047, + 1046, + 1079, + 1080, + 541, + 543, + 544, + 542, + 121, + 951, + 953, + 753, + 120, + 947, + 949, + 950, + 948, + 348, + 869, + 545, + 547, + 546, + 911, + 946, + 1081, + 1082, + 641, + 642, + 643, + 644, + 645, + 646, + 647, + 648, + 9, + 1034, + 1036, + 1037, + 1035, + 745, + 746, + 747, + 748, + 1033, + 1038, + 1032, + 600, + 262, + 259, + 260, + 171, + 170, + 293, + 261, + 168, + 169, + 477, + 562, + 563, + 447, + 16, + 564, + 15, + 196, + 212, + 539, + 444, + 14, + 445, + 13 +] + +# with open('cams.json', 'w') as out: +# out.write('[') +# for id in ids: +# r = requests.get(f'https://images.drivebc.ca/webcam/api/v1/webcams/{id}') +# data = r.json() +# # if data['dbcMark'] != 'DriveBC.ca' or data['credit']: +# # pprint(f"{data['dbcMark']} {data['credit']}") +# # break +# out.write(json.dumps(data)) +# out.write(',') +# out.write(']') + +cams = json.load(open('cams.json')) +credits = [] +for cam in cams: + # if cam['dbcMark'] != 'DriveBC.ca' or cam['credit']: + # pprint({ + # 'id': cam['id'], + # 'dbcMark': cam['dbcMark'], + # 'credit': cam['credit'], + # }) + if cam['credit']: + credit = cam['credit'] + # credit = re.sub(NEWLINES, ' ', credit, re.M) + credit = re.sub(HTML, '', credit) + credit = credit.replace(' ', ' ') + if credit not in credits: + credits.append(credit) +for credit in credits: + print(credit) + diff --git a/src/images/webcams/.gitignore b/src/images/webcams/.gitignore new file mode 100644 index 000000000..bae482515 --- /dev/null +++ b/src/images/webcams/.gitignore @@ -0,0 +1,6 @@ +# this file ensures that the images/webcams folder is present, avoiding +# breaking errors in dev environments, while ignoring actual cam images +*.jpg +*.jpeg +*.gif +*.png \ No newline at end of file