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
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"},
]
[[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]]
name = "idna"
version = "3.10"
@ -139,6 +154,20 @@ files = [
[package.extras]
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]]
name = "python-slugify"
version = "8.0.4"
@ -156,6 +185,44 @@ text-unidecode = ">=1.3"
[package.extras]
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]]
name = "requests"
version = "2.32.3"
@ -177,6 +244,17 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
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]]
name = "text-unidecode"
version = "1.3"
@ -218,4 +296,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "cbcfeef2301c00347932493e38948d8930eb3e2a1cc65f1781d86d50614ac3f6"
content-hash = "7bc68de8c28b8a00a3e83ae30a243333e6e6339b159f0e5593e8325659cb57ae"

View File

@ -13,7 +13,7 @@ class Unsplash(Provider):
super().__init__(settings, session)
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}")
response = self.session.get(query).text
@ -25,7 +25,8 @@ class Unsplash(Provider):
image_slug = matches.group(1)
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}")
title = image_slug.replace("-", " ").replace(image_id, "").strip().title()

View File

@ -1,6 +1,7 @@
import datetime
import logging
import re
import time
from providers._provider import Provider
# https://api.wikimedia.org/wiki/Feed_API/Reference/Featured_content
@ -13,19 +14,24 @@ class Wikimedia(Provider):
super().__init__(settings, session)
def get_image_info(self):
today = datetime.datetime.now()
date = today.strftime('%Y/%m/%d')
logging.debug(f"Date: {date}")
url = 'https://api.wikimedia.org/feed/v1/wikipedia/en/featured/' + date
logging.debug(f"URL: {url}")
# Since wikipedia API seems to fail to provide image url, we will retry on key error until we get the image url
try:
today = datetime.datetime.now()
date = today.strftime('%Y/%m/%d')
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()
# logging.debug(f"Response: {response}")
response = self.session.get(url).json()
# logging.debug(f"Response: {response}")
image = response['image']['image']['source']
logging.debug(f"Image: {image}")
image_url = image
image = response['image']['image']['source']
logging.debug(f"Image: {image}")
image_url = image
title = response['image']['description']['text']
return image_url, title
title = response['image']['description']['text']
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]
name = "daily-wallpaper"
name = "DailyWallpaper"
version = "0.1.0"
description = ""
authors = ["Isaac Shoebottom <ir.shoebottom@gmail.com>"]
readme = "README.md"
packages = [
{ include = "providers"},
{ include = "settings"},
{ include = "app"}
]
[tool.poetry.dependencies]
python = "^3.11"
@ -11,7 +16,20 @@ requests = "^2.32"
urllib3 = "^1.26"
python-slugify = "^8.0"
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]
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():
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["location"].comment("Download location")
general.add("provider", "bing")
general["provider"].comment("Which wallpaper provider to use")
general.add("provider", ["bing", "unsplash", "wikimedia"])
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.value.item("include_title").comment("Include image title in filename")
general.add("set_wallpaper", False)
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.value.item("log").comment("Log to file, located in the download location")
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.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.add("size", "UHD")
bing["size"].comment('Image size, possible values: "UHD", "1920x1080"')
@ -56,6 +59,7 @@ def default_settings():
defaults = tomlkit.document()
defaults.add("general", general)
defaults.add("daemon", daemon)
defaults.add("bing", bing)
defaults.add("unsplash", unsplash)
defaults.add("wikimedia", wikimedia)