damongolding / immich-kiosk

Immich Kiosk is a lightweight slideshow for running on kiosk devices and browsers that uses Immich as a data source.
GNU Affero General Public License v3.0
532 stars 19 forks source link

Specify Multiple Accounts #82

Closed photomatix18 closed 2 months ago

photomatix18 commented 2 months ago

This may already be possible and I just missed it in the documentation, but is there a way to specify multiple accounts/api's and specify people for each account to pull from both accounts simultaneously? i.g. I would like to show pictures from both my wife's timeline and mine but only of us and our children.

I'm assuming you could do this with a shared album, but "smart" albums aren't a thing yet, correct? So it would have to be manually updated.

jschwalbe commented 2 months ago

I was thinking this exact thing earlier today! Get out of my mind! :D

damongolding commented 2 months ago

It's not possible at the moment. I think it would require substantial changes to how the config works to implement this. Though I do see the benefit.

jschwalbe commented 2 months ago

Good point. You'd have to either split up the config into multiple files (sort of like how nginx does? config1.yaml, config2.yaml, ...?), maybe with one "main" config with all the regular settings and then an additional file for each api_key or you'd have to start doing more indenting and making the yaml confugly (I really dislike yaml!).

damongolding commented 2 months ago

Good point. You'd have to either split up the config into multiple files (sort of like how nginx does? config1.yaml, config2.yaml, ...?), maybe with one "main" config with all the regular settings and then an additional file for each api_key or you'd have to start doing more indenting and making the yaml confugly (I really dislike yaml!).

I used to really dislike yaml but it's much better then JSON for configs imo. I'm also quite fond of TOML.

damongolding commented 2 months ago

multiple configs sounds interesting or maybe "profiles".

then use: http://{KIOSK_URL}?profile=1 or http://{KIOSK_URL}?profile=CUSTOM_NAME

with a possible: http://{KIOSK_URL}?profile=random

but it raises the issue of album/people params in the url 😵‍💫

jschwalbe commented 2 months ago

Not my baby but I think I'd avoid that :) If someone really wants profiles they can spin up another version on a different port. That's getting too far into the weeds imho

damongolding commented 2 months ago

Not my baby but I think I'd avoid that :) If someone really wants profiles they can spin up another version on a different port. That's getting too far into the weeds imho

I am inclined to agree.

photomatix18 commented 2 months ago

Really in the end, if Immich gets the "smart" albums working, that would solve the issue.

damongolding commented 2 months ago

Yup and it’s one of the most requested features so I assume it will be implemented sooner rather then later. On 14 Sep 2024 at 4:47 PM +0100, photomatix18 @.***>, wrote:

Really in the end, if Immich gets the "smart" albums working, that would solve the issue. — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you commented.Message ID: @.***>

lobs323 commented 2 months ago

My solution to this problem is a shared album between my wife and I and a python script that runs on a cron schedule. The script takes our API keys, the people IDs of our son, and a destination album id and then it simply queries the previous days worth of assets for each given API:person pair and adds them to the album.

And then kiosk just points at this album only.

#!/usr/bin/python3
import logging
import json
import argparse
import requests
from datetime import datetime, timedelta, timezone

def __const_modifier_options():
        return "hdw"

def get_calculated_date(shorthand):
        import re
        shorthand = str(shorthand)
        pattern=r'^(?P<integer>[0-9]{1,})(?P<modifier>['+f'{__const_modifier_options()}'+r'])$'
        re_pattern=re.compile(pattern, re.IGNORECASE)
        try:
                assert(re_pattern.match(shorthand)),f'{shorthand} failed to pass validation'
                match_dict = re_pattern.search(shorthand).groupdict()
                integer = int(match_dict["integer"])
                modifier = match_dict["modifier"]
                now = datetime.now(timezone.utc)
                match modifier.lower():
                        case "h":
                                return now - timedelta(hours=integer)
                        case "d":
                                return now - timedelta(days=integer)
                        case "w":
                                return now - timedelta(weeks=integer)
                '''     case "m":
                                return now - timedelta(months=integer)
                        case "y":
                                return now - timedelta(years=integer)''' # unsupported
        except AssertionError as e:
                print(e)
                exit(1)

def get_headers(api_key):
        return { "Content-Type": "application/json", "accept" : "application/json", "x-api-key": api_key }

def get_timeline_buckets(host, api_key, params):
        url = f'api/timeline/buckets'
        uri = f'http://{host}/{url}'
        headers = get_headers(api_key)
        response = requests.get(uri, params=params, headers=headers)
        return response.json()

def get_timeline_bucket(host, api_key, params):
        url = f'api/timeline/bucket'
        uri = f'http://{host}/{url}'
        headers = get_headers(api_key)
        response = requests.get(uri, params=params, headers=headers)
        return response.json()

def add_assets_to_album(host, api_key, destination, assets):
        url = f'api/albums/{destination}/assets'
        uri = f'http://{host}/{url}'
        headers = get_headers(api_key)
        payload = json.dumps({"ids" : assets})
        response = requests.request("PUT", uri, headers=headers, data=payload)
        return

def get_assets_for_person(host, api_key, person_uid, date_from=None):
        params = {"isArchived":"false","personId":person_uid,"size":"MONTH"}
        if date_from: # there's only DAY and MONTH available
                if date_from >= (datetime.now(timezone.utc) - timedelta(days=31)):
                        params["size"] = "DAY"
        asset_ids = []
        buckets = get_timeline_buckets(host, api_key, params)
        for bucket in buckets:
                if date_from:
                        if datetime.strptime(bucket["timeBucket"], "%Y-%m-%dT%H:%M:%S.%f%z") < date_from: break
                params["timeBucket"] = bucket["timeBucket"]
                bucket = get_timeline_bucket(host, api_key, params)
                asset_ids += [ asset["id"] for asset in bucket ]
        return asset_ids

if __name__ == "__main__":
        logging.basicConfig(level=logging.DEBUG)
        logger = logging.getLogger()
        logger.setLevel(logging.DEBUG)

        parser = argparse.ArgumentParser()
        parser.add_argument("-i","--immich-host"
                ,help="Input -h 127.0.0.1:2283 or wherever your instance is hosted"
                ,required=True
        )

        parser.add_argument("-p","--person"
                ,action="append"
                ,nargs=2
                ,metavar=("api_key","person_uid")
                ,help="Input -p api_key person_uid for each account you want to merge into an album"
                ,required=True
        )

        parser.add_argument("-d","--destination"
                ,required=True
                ,help="Input -d album_uid to set the destination where assets are to be added to. This album has to be shared to all members that this script executes as."
        )

        parser.add_argument("-df","--date-from"
                ,required=False
                ,help="Input a shorthand format to get assets from a calculated timeframe. Not including this option means that all assets from all time will be obtained. The accepted shorthand format is <integer><modifier> where modifier can be [h(our)/d(ay)/w(eek)]. I.e. 7d means 7 days ago. 3h means 3 hours ago."
        )

        args = parser.parse_args()

        host = args.immich_host
        dest = args.destination

        date = get_calculated_date(args.date_from) if args.date_from else None

        for set in args.person:
                api_key, puid = (set[0], set[1])
                assets = get_assets_for_person(host, api_key, puid, date_from=date)
                print(f"adding {len(assets)} assets to the destination album")
                add_assets_to_album(host, api_key, dest, assets)
damongolding commented 2 months ago

Nice and resourceful! I did see another Python script that was similar to this. It utilised the searchMetaData api endpoint so I was investigating  how they used it. On 15 Sep 2024 at 12:10 PM +0100, lobs323 @.***>, wrote:

My solution to this problem is a shared album between my wife and I and a python script that runs on a cron schedule. The script takes our API keys, the people IDs of our son, and a destination album id and then it simply queries the previous days worth of assets for each given API:person pair and adds them to the album. And then kiosk just points at this album only.

!/usr/bin/python3

import logging import json import argparse import requests from datetime import datetime, timedelta, timezone

def __const_modifier_options(): return "hdw"

def get_calculated_date(shorthand): import re shorthand = str(shorthand) pattern=r'^(?P[0-9]{1,})(?P['+f'{__const_modifier_options()}'+r'])$' re_pattern=re.compile(pattern, re.IGNORECASE) try: assert(re_pattern.match(shorthand)),f'{shorthand} failed to pass validation' match_dict = re_pattern.search(shorthand).groupdict() integer = int(match_dict["integer"]) modifier = match_dict["modifier"] now = datetime.now(timezone.utc) match modifier.lower(): case "h": return now - timedelta(hours=integer) case "d": return now - timedelta(days=integer) case "w": return now - timedelta(weeks=integer) ''' case "m": return now - timedelta(months=integer) case "y": return now - timedelta(years=integer)''' # unsupported except AssertionError as e: print(e) exit(1)

def get_headers(api_key): return { "Content-Type": "application/json", "accept" : "application/json", "x-api-key": api_key }

def get_timeline_buckets(host, api_key, params): url = f'api/timeline/buckets' uri = f'http://{host}/{url}' headers = get_headers(api_key) response = requests.get(uri, params=params, headers=headers) return response.json()

def get_timeline_bucket(host, api_key, params): url = f'api/timeline/bucket' uri = f'http://{host}/{url}' headers = get_headers(api_key) response = requests.get(uri, params=params, headers=headers) return response.json()

def add_assets_to_album(host, api_key, destination, assets): url = f'api/albums/{destination}/assets' uri = f'http://{host}/{url}' headers = get_headers(api_key) payload = json.dumps({"ids" : assets}) response = requests.request("PUT", uri, headers=headers, data=payload) return

def get_assets_for_person(host, api_key, person_uid, date_from=None): params = {"isArchived":"false","personId":person_uid,"size":"MONTH"} if date_from: # there's only DAY and MONTH available if date_from >= (datetime.now(timezone.utc) - timedelta(days=31)): params["size"] = "DAY" asset_ids = [] buckets = get_timeline_buckets(host, api_key, params) for bucket in buckets: if date_from: if datetime.strptime(bucket["timeBucket"], "%Y-%m-%dT%H:%M:%S.%f%z") < date_from: break params["timeBucket"] = bucket["timeBucket"] bucket = get_timeline_bucket(host, api_key, params) asset_ids += [ asset["id"] for asset in bucket ] return asset_ids

if name == "main": logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger() logger.setLevel(logging.DEBUG)

   parser = argparse.ArgumentParser()
   parser.add_argument("-i","--immich-host"
           ,help="Input -h 127.0.0.1:2283 or wherever your instance is hosted"
           ,required=True
   )

   parser.add_argument("-p","--person"
           ,action="append"
           ,nargs=2
           ,metavar=("api_key","person_uid")
           ,help="Input -p api_key person_uid for each account you want to merge into an album"
           ,required=True
   )

   parser.add_argument("-d","--destination"
           ,required=True
           ,help="Input -d album_uid to set the destination where assets are to be added to. This album has to be shared to all members that this script executes as."
   )

   parser.add_argument("-df","--date-from"
           ,required=False
           ,help="Input a shorthand format to get assets from a calculated timeframe. Not including this option means that all assets from all time will be obtained. The accepted shorthand format is <integer><modifier> where modifier can be [h(our)/d(ay)/w(eek)]. I.e. 7d means 7 days ago. 3h means 3 hours ago."
   )

   args = parser.parse_args()

   host = args.immich_host
   dest = args.destination

   date = get_calculated_date(args.date_from) if args.date_from else None

   for set in args.person:
           api_key, puid = (set[0], set[1])
           assets = get_assets_for_person(host, api_key, puid, date_from=date)
           print(f"adding {len(assets)} assets to the destination album")
           add_assets_to_album(host, api_key, dest, assets)

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you modified the open/close state.Message ID: @.***>