Compare commits

...

8 Commits

9 changed files with 240 additions and 77 deletions

View File

@ -9,4 +9,13 @@ Unsplash Applications
- https://unsplash.com/oauth/applications - https://unsplash.com/oauth/applications
Wikipedia API Docs Wikipedia API Docs
- https://api.wikimedia.org/wiki/Getting_featured_content_from_Wikipedia_with_Python#Today's_featured_article - https://api.wikimedia.org/wiki/Getting_featured_content_from_Wikipedia_with_Python#Today's_featured_article
# TODO
- [ ] Add more providers
- [ ] Deal with existing todos in code
- [ ] Add more error handling
- [x] Make logging to file work
- [ ] Add tests
- [ ] Add more documentation
- [x] Add auto setting of wallpaper

97
app/main.py Normal file
View File

@ -0,0 +1,97 @@
import datetime
import os
import requests
import importlib
import logging
import slugify
import time
import croniter
import win32gui
import win32con
from settings import settings
def main():
config = settings.load_settings()
log_path = os.path.abspath(os.path.expanduser(config['general']['location'] + "/log.txt"))
if config['general']['log']:
logging.basicConfig(filename=log_path, level=config['general']['log_level'], format="%(asctime)s [%(levelname)s] %(message)s")
else:
logging.basicConfig(level=config['general']['log_level'], format="%(asctime)s [%(levelname)s] %(message)s")
logging.debug(f"Config: {config}")
chosen_providers = config['general']['provider']
# Daemon mode
if config['daemon']['daemon']:
while True:
for provider in chosen_providers:
if provider == config['general']['provider'][0]:
download_with_provider(provider, config, set_wallpaper=True)
download_with_provider(provider, config)
if config['daemon']['cron'] != "":
now = datetime.datetime.now()
cron = croniter.croniter(config['daemon']['cron'], now)
next_run = cron.get_next(datetime.datetime)
logging.info(f"Next run: {next_run}")
time.sleep((next_run - now).total_seconds())
else:
time.sleep(config['daemon']['interval'])
# Download once
else:
for provider in chosen_providers:
if provider == config['general']['provider'][0]:
download_with_provider(provider, config, set_wallpaper=True)
download_with_provider(provider, config)
def download_with_provider(provider_name, config, set_wallpaper=False):
session = requests.Session()
session.headers.update({
"User-Agent": config['general']['user_agent']
})
# Convenience variables, could be inlined
provider_settings = config[provider_name] if provider_name in config else None
download_location = os.path.abspath(os.path.expanduser(config['general']['location']))
# Load the provider module
provider = importlib.import_module(f"providers.{provider_name}")
# Create an instance of the provider
provider_obj = getattr(provider, provider_name.title())(provider_settings, session)
# Get the image URL and title
image_url, image_title = provider_obj.get_image_info()
logging.debug(f"Image URL: {image_url}")
# Variables for the file path
date = datetime.datetime.now().strftime("%Y-%m-%d")
image_title = slugify.slugify(image_title)
file_path = f"{download_location}/{provider_name.title()}/{date} [{image_title}].jpg"
# Check if we should include the title in the filename
if not config['general']['include_title']:
file_path = f"{download_location}/{provider_name.title()}/{date}.jpg"
# Create the download location if it doesn't exist
if not os.path.exists(download_location):
logging.info(f"Creating download location: {download_location}")
os.mkdir(download_location)
if not os.path.exists(f"{download_location}/{provider_name.title()}"):
logging.info(f"Creating provider location: {download_location}/{provider_name.title()}")
os.mkdir(f"{download_location}/{provider_name.title()}")
# Check if the file exists and if we should overwrite it
if os.path.exists(file_path) and not config['general']['overwrite']:
logging.info(f"File exists, skipping: {file_path}")
return
# Download the image
logging.info(f"Downloading image: {image_title}")
image = session.get(image_url).content
logging.info(f"Saving file: {file_path}")
with open(file_path, "wb") as file:
file.write(image)
if config['general']['set_wallpaper'] and set_wallpaper:
logging.info("Setting wallpaper")
win32gui.SystemParametersInfo(win32con.SPI_SETDESKWALLPAPER, file_path, win32con.SPIF_SENDCHANGE)
if __name__ == "__main__":
main()

View File

@ -1,50 +0,0 @@
import datetime
import os
import sys
import requests
import importlib
import logging
import slugify
import settings
def main():
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
config = settings.load_settings()
logging.debug(f"Config: {config}")
session = requests.Session()
session.headers.update({
"User-Agent": config['general']['user_agent']
})
# Convenience variables, could be inlined
provider_name = config['general']['provider']
provider_settings = config[config['general']['provider']]
download_location = os.path.abspath(os.path.expanduser(config['general']['location']))
# Load the provider module
provider = importlib.import_module(f"providers.{config['general']['provider']}")
# Create an instance of the provider
provider_obj = getattr(provider, provider_name.title())(provider_settings, session)
# Get the image URL and title
image_url, image_title = provider_obj.get_image_info()
logging.debug(f"Image URL: {image_url}")
# Download the image
image = session.get(image_url).content
if not os.path.exists(download_location):
os.mkdir(download_location)
if not os.path.exists(f"{download_location}/{provider_name.title()}"):
os.mkdir(f"{download_location}/{provider_name.title()}")
date = datetime.datetime.now().strftime("%Y-%m-%d")
image_title = slugify.slugify(image_title)
with open(f"{download_location}/{provider_name.title()}/{date} [{image_title}].jpg", "wb") as file:
file.write(image)
if __name__ == "__main__":
main()

80
poetry.lock generated
View File

@ -125,6 +125,21 @@ files = [
{file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
] ]
[[package]]
name = "croniter"
version = "5.0.1"
description = "croniter provides iteration for datetime object with cron like format"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6"
files = [
{file = "croniter-5.0.1-py2.py3-none-any.whl", hash = "sha256:eb28439742291f6c10b181df1a5ecf421208b1fc62ef44501daec1780a0b09e9"},
{file = "croniter-5.0.1.tar.gz", hash = "sha256:7d9b1ef25b10eece48fdf29d8ac52f9b6252abff983ac614ade4f3276294019e"},
]
[package.dependencies]
python-dateutil = "*"
pytz = ">2021.1"
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@ -139,6 +154,20 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
six = ">=1.5"
[[package]] [[package]]
name = "python-slugify" name = "python-slugify"
version = "8.0.4" version = "8.0.4"
@ -156,6 +185,44 @@ text-unidecode = ">=1.3"
[package.extras] [package.extras]
unidecode = ["Unidecode (>=1.1.1)"] unidecode = ["Unidecode (>=1.1.1)"]
[[package]]
name = "pytz"
version = "2024.2"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
files = [
{file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
{file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
]
[[package]]
name = "pywin32"
version = "308"
description = "Python for Window Extensions"
optional = false
python-versions = "*"
files = [
{file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"},
{file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"},
{file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"},
{file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"},
{file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"},
{file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"},
{file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"},
{file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"},
{file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"},
{file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"},
{file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"},
{file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"},
{file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"},
{file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"},
{file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"},
{file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"},
{file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"},
{file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"},
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"
@ -177,6 +244,17 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]] [[package]]
name = "text-unidecode" name = "text-unidecode"
version = "1.3" version = "1.3"
@ -218,4 +296,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "cbcfeef2301c00347932493e38948d8930eb3e2a1cc65f1781d86d50614ac3f6" content-hash = "7bc68de8c28b8a00a3e83ae30a243333e6e6339b159f0e5593e8325659cb57ae"

View File

@ -13,7 +13,7 @@ class Unsplash(Provider):
super().__init__(settings, session) super().__init__(settings, session)
def get_image_info(self): def get_image_info(self):
query = "https://unsplash.com/collections/1459961/photo-of-the-day-(archive)" query = f"https://unsplash.com/collections/{self.settings['collection']}"
logging.debug(f"Query: {query}") logging.debug(f"Query: {query}")
response = self.session.get(query).text response = self.session.get(query).text
@ -25,7 +25,8 @@ class Unsplash(Provider):
image_slug = matches.group(1) image_slug = matches.group(1)
logging.debug(f"Image slug: {image_slug}") logging.debug(f"Image slug: {image_slug}")
image_id = image_slug.split("-")[-1] # Last 11 characters are the image ID
image_id = image_slug[-11:]
logging.debug(f"Image ID: {image_id}") logging.debug(f"Image ID: {image_id}")
title = image_slug.replace("-", " ").replace(image_id, "").strip().title() title = image_slug.replace("-", " ").replace(image_id, "").strip().title()

View File

@ -1,6 +1,7 @@
import datetime import datetime
import logging import logging
import re import time
from providers._provider import Provider from providers._provider import Provider
# https://api.wikimedia.org/wiki/Feed_API/Reference/Featured_content # https://api.wikimedia.org/wiki/Feed_API/Reference/Featured_content
@ -13,19 +14,24 @@ class Wikimedia(Provider):
super().__init__(settings, session) super().__init__(settings, session)
def get_image_info(self): def get_image_info(self):
today = datetime.datetime.now() # Since wikipedia API seems to fail to provide image url, we will retry on key error until we get the image url
date = today.strftime('%Y/%m/%d') try:
logging.debug(f"Date: {date}") today = datetime.datetime.now()
url = 'https://api.wikimedia.org/feed/v1/wikipedia/en/featured/' + date date = today.strftime('%Y/%m/%d')
logging.debug(f"URL: {url}") logging.debug(f"Date: {date}")
url = 'https://api.wikimedia.org/feed/v1/wikipedia/en/featured/' + date
logging.debug(f"URL: {url}")
response = self.session.get(url).json() response = self.session.get(url).json()
# logging.debug(f"Response: {response}") # logging.debug(f"Response: {response}")
image = response['image']['image']['source'] image = response['image']['image']['source']
logging.debug(f"Image: {image}") logging.debug(f"Image: {image}")
image_url = image image_url = image
title = response['image']['description']['text'] title = response['image']['description']['text']
return image_url, title
return image_url, title except KeyError:
logging.error("KeyError, retrying...")
time.sleep(10) # Wait 10 seconds
return self.get_image_info()

View File

@ -1,9 +1,14 @@
[tool.poetry] [tool.poetry]
name = "daily-wallpaper" name = "DailyWallpaper"
version = "0.1.0" version = "0.1.0"
description = "" description = ""
authors = ["Isaac Shoebottom <ir.shoebottom@gmail.com>"] authors = ["Isaac Shoebottom <ir.shoebottom@gmail.com>"]
readme = "README.md" readme = "README.md"
packages = [
{ include = "providers"},
{ include = "settings"},
{ include = "app"}
]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.11"
@ -11,7 +16,20 @@ requests = "^2.32"
urllib3 = "^1.26" urllib3 = "^1.26"
python-slugify = "^8.0" python-slugify = "^8.0"
tomlkit = "^0.13.2" tomlkit = "^0.13.2"
croniter = "^5.0"
pywin32 = "^308"
[tool.poetry.scripts]
DailyWallpaper = "app.main:main"
# https://pypi.org/project/poetry-pyinstaller-plugin/
# https://stackoverflow.com/a/78050613
[tool.poetry-pyinstaller-plugin.package]
providers = "."
settings = "."
[tool.poetry-pyinstaller-plugin.scripts]
DailyWallpaper = { source = "app/main.py", type = "onefile", bundle = false }
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

0
settings/__init__.py Normal file
View File

View File

@ -6,20 +6,14 @@ user_path = os.path.expanduser("~/.config/daily-wallpaper/config.toml")
def default_settings(): def default_settings():
general = tomlkit.table() general = tomlkit.table()
general.add("interval", 86400)
general["interval"].comment("Interval in seconds (24 hours)")
general.add("start", 7200)
general["start"].comment("Start time in seconds (2:00 AM)")
general.add("location", "~/Pictures/Wallpapers") general.add("location", "~/Pictures/Wallpapers")
general["location"].comment("Download location") general["location"].comment("Download location")
general.add("provider", "bing") general.add("provider", ["bing", "unsplash", "wikimedia"])
general["provider"].comment("Which wallpaper provider to use") general["provider"].comment("Which wallpaper provider to use, in order of preference (do no include if you don't want to download the file)")
general.add("include_title", True) general.add("include_title", True)
general.value.item("include_title").comment("Include image title in filename") general.value.item("include_title").comment("Include image title in filename")
general.add("set_wallpaper", False) general.add("set_wallpaper", False)
general.value.item("set_wallpaper").comment("Set wallpaper after download") general.value.item("set_wallpaper").comment("Set wallpaper after download")
general.add("daemon", False)
general.value.item("daemon").comment("Run as daemon (continuously in the background)")
general.add("log", False) general.add("log", False)
general.value.item("log").comment("Log to file, located in the download location") general.value.item("log").comment("Log to file, located in the download location")
general.add("log_level", "INFO") general.add("log_level", "INFO")
@ -30,6 +24,15 @@ def default_settings():
general["user_agent"].comment("User-Agent to use for requests, change to avoid being blocked or to comply with ToS") general["user_agent"].comment("User-Agent to use for requests, change to avoid being blocked or to comply with ToS")
general.add(tomlkit.nl()) general.add(tomlkit.nl())
daemon = tomlkit.table()
daemon.add("daemon", False)
daemon.value.item("daemon").comment("Run as daemon (continuously in the background)")
daemon.add("interval", 86400)
daemon["interval"].comment("Interval in seconds (24 hours)")
daemon.add("cron", "0 0 * * *")
daemon["cron"].comment("Cron expression, overrides interval")
daemon.add(tomlkit.nl())
bing = tomlkit.table() bing = tomlkit.table()
bing.add("size", "UHD") bing.add("size", "UHD")
bing["size"].comment('Image size, possible values: "UHD", "1920x1080"') bing["size"].comment('Image size, possible values: "UHD", "1920x1080"')
@ -56,6 +59,7 @@ def default_settings():
defaults = tomlkit.document() defaults = tomlkit.document()
defaults.add("general", general) defaults.add("general", general)
defaults.add("daemon", daemon)
defaults.add("bing", bing) defaults.add("bing", bing)
defaults.add("unsplash", unsplash) defaults.add("unsplash", unsplash)
defaults.add("wikimedia", wikimedia) defaults.add("wikimedia", wikimedia)