sunrise-sunet: commit basic data and scraping code
This commit is contained in:
parent
897d859d21
commit
363051c35e
17 changed files with 2158 additions and 6 deletions
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
"type": "module",
|
||||
"dependencies": {
|
||||
"date-fns": "^2.29.2",
|
||||
"just-shuffle": "^4.2.0",
|
||||
"leaflet": "^1.8.0",
|
||||
"mdsvex": "^0.10.5",
|
||||
"mongodb": "^4.8.1",
|
||||
|
|
|
|||
6
prettierrc
Normal file
6
prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
1
scripts/sunrise_sunset_images/.gitignore
vendored
Normal file
1
scripts/sunrise_sunset_images/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
environment_variables.py
|
||||
69
scripts/sunrise_sunset_images/create_day_photo_mapping.py
Normal file
69
scripts/sunrise_sunset_images/create_day_photo_mapping.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import json
|
||||
import datetime
|
||||
import utils
|
||||
import photos
|
||||
|
||||
|
||||
|
||||
class DailyPhoto:
|
||||
def __init__(self, date: datetime.date, photo: photos.SunriseOrSunsetPhoto):
|
||||
self.date = date
|
||||
self.photo = photo
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
return {
|
||||
"date": self.date.strftime("%Y-%m-%d"),
|
||||
"photo": self.photo.as_json()
|
||||
}
|
||||
|
||||
class DailyPhotoSet:
|
||||
def __init__(self, start_date: datetime.date = datetime.date(2023, 1, 21)):
|
||||
self._date = start_date
|
||||
self._photos = []
|
||||
|
||||
def add_photo(self, photo: photos.SunriseOrSunsetPhoto):
|
||||
self._photos.append(DailyPhoto(self._date, photo))
|
||||
self.photos.sort(key=lambda photo: photo.photo.id)
|
||||
self._date += datetime.timedelta(days=1)
|
||||
|
||||
@property
|
||||
def photos(self) -> list[DailyPhoto]:
|
||||
return list(sorted(self._photos, key=lambda photo: photo.date))
|
||||
|
||||
@property
|
||||
def current_date(self) -> datetime.date:
|
||||
return self._date;
|
||||
|
||||
def as_json(self) -> str:
|
||||
return json.dumps({
|
||||
"photos": [photo.as_dict() for photo in self.photos]
|
||||
})
|
||||
|
||||
|
||||
def get_all_photos() -> list[photos.SunriseOrSunsetPhoto]:
|
||||
file_names: list[str] = ["data/all_photos-0.json", "data/all_photos-1.json"]
|
||||
sunrise_or_sunset_photos = photos.SunriseOrSunsetPhotoSet.from_no_data();
|
||||
for file_name in file_names:
|
||||
with open(utils.make_relative_file_name(file_name), "r") as infile:
|
||||
photo_set = photos.SunriseOrSunsetPhotoSet.from_json(infile.read())
|
||||
sunrise_or_sunset_photos.add_photos(photo_set.unique_photos)
|
||||
|
||||
return sunrise_or_sunset_photos.photos_sorted_by_id
|
||||
|
||||
|
||||
def main():
|
||||
print("Creating daily photo mapping...")
|
||||
all_photos = get_all_photos()
|
||||
print("Found {} photos".format(len(all_photos)))
|
||||
|
||||
daily_photos = DailyPhotoSet()
|
||||
|
||||
for photo in all_photos:
|
||||
daily_photos.add_photo(photo)
|
||||
|
||||
|
||||
with open(utils.make_relative_file_name('data/daily-photos.json'), 'w') as outfile:
|
||||
outfile.write(daily_photos.as_json())
|
||||
|
||||
main()
|
||||
|
||||
1724
scripts/sunrise_sunset_images/data/all_photos.json
Normal file
1724
scripts/sunrise_sunset_images/data/all_photos.json
Normal file
File diff suppressed because it is too large
Load diff
37
scripts/sunrise_sunset_images/get_sunrise_images.py
Normal file
37
scripts/sunrise_sunset_images/get_sunrise_images.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import environment_variables
|
||||
import unsplash_api_gateway
|
||||
import photos
|
||||
import utils
|
||||
import json
|
||||
|
||||
def main():
|
||||
"""Get sunrise and sunset images from Unsplash API."""
|
||||
api = unsplash_api_gateway.UnsplashApi(environment_variables.UNSPLASH_ACCESS_KEY, environment_variables.UNSPLASH_SECRET_KEY)
|
||||
|
||||
sunrise_images = api.search_results_for_query(query="sunrise", page_size=100, page_number=5)
|
||||
sunset_images = api.search_results_for_query(query="sunset", page_size=100, page_number=5)
|
||||
|
||||
unfiltered_length = len(sunrise_images.unfiltered_results) + len(sunset_images.unfiltered_results)
|
||||
filtered_length = len(sunrise_images.results) + len(sunset_images.results)
|
||||
print(f"Found {unfiltered_length} unfiltered images...")
|
||||
print(f"Found {filtered_length} filtered images")
|
||||
|
||||
all_photos = photos.SunriseOrSunsetPhotoSet.from_unsplash_search_results(sunrise_images, sunset_images)
|
||||
file_name = utils.make_relative_file_name("data/all_photos.json")
|
||||
|
||||
print(f"Found {len(all_photos.photos)} photos")
|
||||
|
||||
file_contents = json.loads(open(file_name, "r").read())
|
||||
with open(file_name, "w") as outfile:
|
||||
photos_in_file = [photos.SunriseOrSunsetPhoto.from_json(photo) for photo in file_contents["photos"]]
|
||||
print(f"Found {len(photos_in_file)} photos in {file_name}...")
|
||||
|
||||
photos_without_opposite_daytime = [photo for photo in photos_in_file if not photo.does_description_contain_opposite_daytime]
|
||||
print(f"Found {len(photos_without_opposite_daytime)} photos without opposite daytime...")
|
||||
|
||||
all_photos.add_photos(photos_without_opposite_daytime)
|
||||
|
||||
print(f"Writing {len(all_photos.photos)} photos to {file_name}")
|
||||
outfile.write(all_photos.as_json())
|
||||
|
||||
main()
|
||||
102
scripts/sunrise_sunset_images/photos.py
Normal file
102
scripts/sunrise_sunset_images/photos.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import json
|
||||
import unsplash_search_results
|
||||
|
||||
class SunriseOrSunsetPhoto:
|
||||
def __init__(
|
||||
self,
|
||||
unsplash_search_result: unsplash_search_results.UnsplashSearchResult,
|
||||
sunrise_or_sunset: str
|
||||
):
|
||||
self.id = unsplash_search_result.id
|
||||
self.description = unsplash_search_result.description
|
||||
self.username = unsplash_search_result.username
|
||||
self.username_url = unsplash_search_result.username_url
|
||||
self.small_url = unsplash_search_result.small_url
|
||||
self.sunrise_or_sunset = sunrise_or_sunset
|
||||
|
||||
@staticmethod
|
||||
def from_json(data: dict) -> "SunriseOrSunsetPhoto":
|
||||
unsplash_search_result = unsplash_search_results.UnsplashSearchResult(
|
||||
data={
|
||||
"id": data["id"],
|
||||
"description": data["description"],
|
||||
"user": {
|
||||
"username": data["username"],
|
||||
"links": {
|
||||
"html": data["username_url"]
|
||||
},
|
||||
},
|
||||
"urls": {
|
||||
"small": data["small_url"]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return SunriseOrSunsetPhoto(
|
||||
unsplash_search_result=unsplash_search_result,
|
||||
sunrise_or_sunset=data["sunrise_or_sunset"],
|
||||
)
|
||||
|
||||
@property
|
||||
def does_description_contain_opposite_daytime(self) -> bool:
|
||||
opposite_word = "sunrise" if self.sunrise_or_sunset == "sunset" else "sunset"
|
||||
return opposite_word in self.description.lower()
|
||||
|
||||
def as_json(self) -> object:
|
||||
return {
|
||||
"id": self.id,
|
||||
"description": self.description,
|
||||
"username": self.username,
|
||||
"username_url": self.username_url,
|
||||
"small_url": self.small_url,
|
||||
"sunrise_or_sunset": self.sunrise_or_sunset
|
||||
}
|
||||
|
||||
class SunriseOrSunsetPhotoSet:
|
||||
def __init__(self, photos: list[SunriseOrSunsetPhoto]):
|
||||
self.photos = photos
|
||||
self.unique_photos = self.get_unique_photos(photos)
|
||||
|
||||
@staticmethod
|
||||
def from_no_data() -> "SunriseOrSunsetPhotoSet":
|
||||
return SunriseOrSunsetPhotoSet(photos=[])
|
||||
|
||||
@staticmethod
|
||||
def from_unsplash_search_results(sunrise_images: unsplash_search_results.UnsplashSearchResultSet, sunset_images: unsplash_search_results.UnsplashSearchResultSet):
|
||||
photos = []
|
||||
photos.extend([SunriseOrSunsetPhoto(result, "sunrise") for result in sunrise_images.results])
|
||||
photos.extend([SunriseOrSunsetPhoto(result, "sunset") for result in sunset_images.results])
|
||||
return SunriseOrSunsetPhotoSet(photos)
|
||||
|
||||
@staticmethod
|
||||
def from_json(json_string: str) -> "SunriseOrSunsetPhotoSet":
|
||||
loaded_json = json.loads(json_string)
|
||||
photos = [SunriseOrSunsetPhoto.from_json(photo) for photo in loaded_json["photos"]]
|
||||
return SunriseOrSunsetPhotoSet(photos)
|
||||
|
||||
def add_photos(self, photos: list[SunriseOrSunsetPhoto]):
|
||||
self.photos.extend(photos)
|
||||
self.unique_photos = self.get_unique_photos(self.photos)
|
||||
|
||||
def get_unique_photos(self, photos: list[SunriseOrSunsetPhoto]) -> list[SunriseOrSunsetPhoto]:
|
||||
"""
|
||||
Some results are duplicates (identified by `id`). This could mean they're
|
||||
not definitely a sunrise or sunset, so neither image should be included.
|
||||
"""
|
||||
all_ids: list[str] = list(map(lambda photo: photo.id, photos))
|
||||
unique_results = []
|
||||
for result in photos:
|
||||
is_id_present_once: bool = 1 == len(list(filter(lambda id: id == result.id, all_ids)))
|
||||
if is_id_present_once:
|
||||
unique_results.append(result)
|
||||
|
||||
return unique_results
|
||||
|
||||
def as_json(self) -> str:
|
||||
return json.dumps({
|
||||
"photos": list(map(lambda photo: photo.as_json(), self.unique_photos))
|
||||
})
|
||||
|
||||
@property
|
||||
def photos_sorted_by_id(self) -> list[SunriseOrSunsetPhoto]:
|
||||
return sorted(self.unique_photos, key=lambda photo: photo.id)
|
||||
11
scripts/sunrise_sunset_images/readme.md
Normal file
11
scripts/sunrise_sunset_images/readme.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Sunrise or Sunset Images
|
||||
|
||||
You need to create an Unsplash developer account with API key ([link](https://unsplash.com/developers)).
|
||||
|
||||
To authenticate calls to the Unsplash API, you need to create a file called `environment_variables.py` to store your access and secret key:
|
||||
|
||||
```py
|
||||
# environment_variables.py
|
||||
UNSPLASH_ACCESS_KEY = "access_key"
|
||||
UNSPLASH_SECRET_KEY="secret_key"
|
||||
```
|
||||
25
scripts/sunrise_sunset_images/unsplash_api_gateway.py
Normal file
25
scripts/sunrise_sunset_images/unsplash_api_gateway.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import requests
|
||||
import unsplash_search_results
|
||||
|
||||
class UnsplashApi:
|
||||
def __init__(self, access_key, secret_key):
|
||||
"""Initialize the Unsplash API wrapper."""
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
|
||||
def search_results_for_query(self, query, page_size=1, page_number=1):
|
||||
"""Search for photos."""
|
||||
response = requests.get(
|
||||
"https://api.unsplash.com/search/photos",
|
||||
params={
|
||||
"query": query,
|
||||
"page": page_number,
|
||||
"per_page": page_size
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Client-ID {self.access_key}"
|
||||
}
|
||||
)
|
||||
|
||||
return unsplash_search_results.UnsplashSearchResultSet(response.json(), query)
|
||||
|
||||
61
scripts/sunrise_sunset_images/unsplash_search_results.py
Normal file
61
scripts/sunrise_sunset_images/unsplash_search_results.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
class UnsplashSearchResult:
|
||||
def __init__(self, data: dict):
|
||||
self.id = data["id"]
|
||||
self._description = data["description"] or "No description"
|
||||
self.username = data["user"]["username"]
|
||||
self.username_url = data["user"]["links"]["html"]
|
||||
self.small_url = data["urls"]["small"]
|
||||
self.json = data
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.description} : {self.small_url} by {self.username}"
|
||||
|
||||
@staticmethod
|
||||
def from_json(data: dict) -> "UnsplashSearchResult":
|
||||
formatted_data = {
|
||||
"id": data["id"],
|
||||
"description": data["description"],
|
||||
"user": {
|
||||
"username": data["username"],
|
||||
"links": {
|
||||
"html": data["username_url"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return UnsplashSearchResult(data=formatted_data)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self._description
|
||||
|
||||
def as_json(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"description": self.description,
|
||||
"username": self.username,
|
||||
"username_url": self.username_url,
|
||||
"small_url": self.small_url
|
||||
}
|
||||
|
||||
|
||||
class UnsplashSearchResultSet:
|
||||
def __init__(self, json, sunrise_or_sunset: str):
|
||||
self.total = json["total"]
|
||||
self.total_pages = json["total_pages"]
|
||||
self.json = json
|
||||
self.unfiltered_results = [UnsplashSearchResult(photo) for photo in json["results"]]
|
||||
self.results = self.filter_out_photos_with_word_in_description("sunrise") if sunrise_or_sunset == "sunrise" else self.filter_out_photos_with_word_in_description("sunset")
|
||||
self.sunrise_or_sunset = sunrise_or_sunset
|
||||
|
||||
def filter_out_photos_with_word_in_description(self, word: str):
|
||||
lower_word = word.lower()
|
||||
return [search_result for search_result in self.unfiltered_results if lower_word not in search_result.description.lower()]
|
||||
|
||||
def as_json(self):
|
||||
return {
|
||||
"total": self.total,
|
||||
"sunrise_or_sunset": self.sunrise_or_sunset,
|
||||
"total_pages": self.total_pages,
|
||||
"results": [result.as_json() for result in self.results]
|
||||
}
|
||||
5
scripts/sunrise_sunset_images/utils.py
Normal file
5
scripts/sunrise_sunset_images/utils.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import os
|
||||
|
||||
def make_relative_file_name(path: str) -> str:
|
||||
dirname = os.path.dirname(__file__)
|
||||
return os.path.join(dirname, path)
|
||||
14
src/lib/sunrise-sunset-photos/index.ts
Normal file
14
src/lib/sunrise-sunset-photos/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export interface SunriseOrSunsetPhotoSet {
|
||||
total: number
|
||||
total_pages: number
|
||||
search_term: string
|
||||
results: SunriseOrSunsetPhoto[]
|
||||
}
|
||||
|
||||
export interface SunriseOrSunsetPhoto {
|
||||
id: string
|
||||
description: string
|
||||
username: string
|
||||
username_url: string
|
||||
small_url: string
|
||||
}
|
||||
10
src/routes/api/sunrise-sunset-photos/+server.ts
Normal file
10
src/routes/api/sunrise-sunset-photos/+server.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { LoadEvent } from '@sveltejs/kit'
|
||||
|
||||
export const GET = async (_event: LoadEvent) => {
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
body: {
|
||||
photos: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
86
src/routes/sunrise-sunset/+page.svelte
Normal file
86
src/routes/sunrise-sunset/+page.svelte
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts">
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const pictures: {
|
||||
imageUrl: string;
|
||||
daytime: 'sunrise' | 'sunset';
|
||||
}[] = [
|
||||
{
|
||||
imageUrl: '/example',
|
||||
daytime: 'sunrise'
|
||||
},
|
||||
{
|
||||
imageUrl: '/examplele',
|
||||
daytime: 'sunset'
|
||||
}
|
||||
];
|
||||
|
||||
const pictureStore = writable(pictures[0]);
|
||||
|
||||
const guessHistory = writable({
|
||||
correct: 0,
|
||||
incorrect: 0,
|
||||
total: 0
|
||||
});
|
||||
|
||||
function onOptionSelected(option: 'sunrise' | 'sunset') {
|
||||
$guessHistory.total += 1;
|
||||
if (option === $pictureStore.daytime) {
|
||||
$guessHistory.correct += 1;
|
||||
} else {
|
||||
$guessHistory.incorrect += 1;
|
||||
}
|
||||
console.log(option);
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1>Sunrise, Sunset?</h1>
|
||||
|
||||
<section class="picture">
|
||||
<img src={$pictureStore.imageUrl} alt="Sunrise or Sunset?" />
|
||||
</section>
|
||||
|
||||
<section class="options">
|
||||
<div class="options__buttons-container">
|
||||
<button class="options__button" on:click={() => onOptionSelected('sunrise')}>Sunrise</button>
|
||||
<button class="options__button" on:click={() => onOptionSelected('sunset')}>Sunset</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="score">
|
||||
{#if $guessHistory.total > 0}
|
||||
<p class="score__text">
|
||||
You've guessed correctly {Number($guessHistory.correct / $guessHistory.total)}% of the time
|
||||
</p>
|
||||
{:else}
|
||||
<p class="score__text">You've not guessed yet.</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss" type="text/postcss">
|
||||
.options {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.options__buttons-container {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.options__button {
|
||||
background-color: #fff;
|
||||
border: 1px solid #000;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.options__button:hover {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,6 +3,7 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
plugins: [sveltekit()],
|
||||
resolve: {}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -1427,6 +1427,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||
|
||||
just-shuffle@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/just-shuffle/-/just-shuffle-4.2.0.tgz#795c09dbac1f9e94e797efd10a7ef537363c8903"
|
||||
integrity sha512-/dDmNseAWLf3XkFY9xf3/BdQoiy27LNUy/7uG4zdSAX526nIHMYPYeJ4pN4lT1/pgNEX8XCXPtUB6gJqTpBEng==
|
||||
|
||||
kleur@^4.1.4, kleur@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
||||
|
|
|
|||
Loading…
Reference in a new issue