diff --git a/backlogger/steam.py b/backlogger/steam.py
new file mode 100644
index 0000000..898ad47
--- /dev/null
+++ b/backlogger/steam.py
@@ -0,0 +1,50 @@
+import re
+import requests
+from urllib.parse import urlencode
+
+STEAM_OPENID_URL = 'https://steamcommunity.com/openid/login'
+STEAM_API_BASE = 'https://api.steampowered.com'
+
+
+def build_auth_url(return_to, realm):
+ params = {
+ 'openid.ns': 'http://specs.openid.net/auth/2.0',
+ 'openid.mode': 'checkid_setup',
+ 'openid.return_to': return_to,
+ 'openid.realm': realm,
+ 'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select',
+ 'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select',
+ }
+ return f"{STEAM_OPENID_URL}?{urlencode(params)}"
+
+
+def verify_and_get_steam_id(params):
+ """Verify OpenID assertion with Steam. Returns steam64 id string or None."""
+ verify_params = {k: v for k, v in params.items()}
+ verify_params['openid.mode'] = 'check_authentication'
+ try:
+ resp = requests.post(STEAM_OPENID_URL, data=verify_params, timeout=10)
+ resp.raise_for_status()
+ except requests.RequestException:
+ return None
+ if 'is_valid:true' not in resp.text:
+ return None
+ claimed_id = params.get('openid.claimed_id', '')
+ match = re.search(r'/openid/id/(\d+)$', claimed_id)
+ return match.group(1) if match else None
+
+
+def get_owned_games(api_key, steam_id):
+ """Return list of games sorted by playtime desc. Raises on API error."""
+ url = f"{STEAM_API_BASE}/IPlayerService/GetOwnedGames/v1/"
+ params = {
+ 'key': api_key,
+ 'steamid': steam_id,
+ 'include_appinfo': 'true',
+ 'include_played_free_games': 'true',
+ 'format': 'json',
+ }
+ resp = requests.get(url, params=params, timeout=15)
+ resp.raise_for_status()
+ games = resp.json().get('response', {}).get('games', [])
+ return sorted(games, key=lambda g: g.get('playtime_forever', 0), reverse=True)
diff --git a/backlogger/templates/backlogger/list.html b/backlogger/templates/backlogger/list.html
index 1006a00..541dd46 100644
--- a/backlogger/templates/backlogger/list.html
+++ b/backlogger/templates/backlogger/list.html
@@ -164,7 +164,13 @@
Backlogger {{ items|length }}
-
+ Add item
+
+ {% if request.GET.imported %}
+
✓ {{ request.GET.imported }} game{{ request.GET.imported|pluralize }} imported
+ {% endif %}
+
▶ Steam
+
+ Add item
+
diff --git a/backlogger/templates/backlogger/steam_import.html b/backlogger/templates/backlogger/steam_import.html
new file mode 100644
index 0000000..1b43c4c
--- /dev/null
+++ b/backlogger/templates/backlogger/steam_import.html
@@ -0,0 +1,248 @@
+
+
+
+
+
+
Import from Steam — Backlogger
+
+
+
+
+
+
+
+
+ {% if error %}
+
{{ error }}
+
+
+ {% elif games %}
+
+
Import from Steam {{ games|length }} games
+
+
+
+
+ {% else %}
+
Import from Steam
+
+ {% endif %}
+
+
+
+
+
+
diff --git a/backlogger/urls.py b/backlogger/urls.py
index c4978d7..b281a66 100644
--- a/backlogger/urls.py
+++ b/backlogger/urls.py
@@ -7,4 +7,7 @@ urlpatterns = [
path('add/', views.item_add, name='add'),
path('
/edit/', views.item_edit, name='edit'),
path('/delete/', views.item_delete, name='delete'),
+ path('steam/login/', views.steam_login, name='steam_login'),
+ path('steam/callback/', views.steam_callback, name='steam_callback'),
+ path('steam/import/', views.steam_import, name='steam_import'),
]
diff --git a/backlogger/views.py b/backlogger/views.py
index 90c09b8..0926380 100644
--- a/backlogger/views.py
+++ b/backlogger/views.py
@@ -1,7 +1,10 @@
+from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404, redirect
+from django.urls import reverse
from .models import Item
from .forms import ItemForm, SignupForm
+from . import steam as steam_api
def signup(request):
@@ -77,3 +80,73 @@ def item_delete(request, pk):
if request.method == 'POST':
get_object_or_404(Item, pk=pk, user=request.user).delete()
return redirect('backlogger:list')
+
+
+@login_required
+def steam_login(request):
+ callback = request.build_absolute_uri(reverse('backlogger:steam_callback'))
+ realm = f"{request.scheme}://{request.get_host()}"
+ return redirect(steam_api.build_auth_url(callback, realm))
+
+
+@login_required
+def steam_callback(request):
+ steam_id = steam_api.verify_and_get_steam_id(request.GET.dict())
+ if not steam_id:
+ return render(request, 'backlogger/steam_import.html', {'error': 'Steam verification failed. Please try again.'})
+
+ api_key = getattr(settings, 'STEAM_API_KEY', '')
+ if not api_key:
+ return render(request, 'backlogger/steam_import.html', {'error': 'Steam API key is not configured on the server.'})
+
+ try:
+ games = steam_api.get_owned_games(api_key, steam_id)
+ except Exception:
+ return render(request, 'backlogger/steam_import.html', {'error': 'Could not fetch your Steam library. Your profile may be set to private.'})
+
+ existing = set(
+ Item.objects.filter(user=request.user, category=Item.GAMES)
+ .values_list('name', flat=True)
+ )
+
+ game_list = []
+ for g in games:
+ name = g.get('name', '')
+ hours = round(g.get('playtime_forever', 0) / 60, 1)
+ game_list.append({
+ 'appid': g.get('appid'),
+ 'name': name,
+ 'hours': hours,
+ 'already_imported': name in existing,
+ })
+
+ request.session['steam_games'] = game_list
+ return render(request, 'backlogger/steam_import.html', {'games': game_list})
+
+
+@login_required
+def steam_import(request):
+ if request.method != 'POST':
+ return redirect('backlogger:list')
+
+ games_by_appid = {str(g['appid']): g for g in request.session.get('steam_games', [])}
+ selected = request.POST.getlist('appids')
+
+ imported = 0
+ for appid in selected:
+ game = games_by_appid.get(appid)
+ if not game or game['already_imported']:
+ continue
+ hours = game['hours']
+ progress = min(100.0, hours) if hours > 0 else 0.0
+ Item.objects.create(
+ user=request.user,
+ category=Item.GAMES,
+ name=game['name'],
+ hours_played=hours,
+ progress_percent=progress,
+ )
+ imported += 1
+
+ del request.session['steam_games']
+ return redirect(f"{reverse('backlogger:list')}?category=games&imported={imported}")
diff --git a/kboris/settings.py b/kboris/settings.py
index 0806b9d..72239a8 100644
--- a/kboris/settings.py
+++ b/kboris/settings.py
@@ -5,6 +5,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'dev-insecure-key')
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
+STEAM_API_KEY = os.environ.get('STEAM_API_KEY', '')
ALLOWED_HOSTS = [
'k-boris.tech',