HEX
Server: Apache/2.4.63 (Unix)
System: Linux Synopilou92 4.4.302+ #72806 SMP Mon Jul 21 23:16:00 CST 2025 x86_64
User: pilou92 (1026)
PHP: 8.0.30
Disabled: NONE
Upload Files
File: /volume1/@appstore/SynologyPhotos/migration/python/dump_photo_station_shared_album.py
#!/usr/bin/env python3
import datetime
import json
import logging
import sys

import pg8000

DOWNLOAD_CONFIG_KEYS = ["allow_user_download", "allow_guest_download"]


class BaseAlbum:
    def __init__(self, cursor):
        self.cursor = cursor
        self.for_admin = False

    def get_album_table(self):
        if self.for_admin:
            return "photo_personal_collection_for_admin"
        return "photo_personal_collection_for_dsm_account"

    def get_photo_relation_table(self):
        if self.for_admin:
            return "photo_personal_collection_photo_for_admin"
        return "photo_personal_collection_photo_for_dsm_account"

    def get_video_relation_table(self):
        if self.for_admin:
            return "photo_personal_collection_video_for_admin"
        return "photo_personal_collection_video_for_dsm_account"

    def get_advanced_table(self):
        if self.for_admin:
            return "photo_advanced_personal_collection_for_admin"
        return "photo_advanced_personal_collection_for_dsm_account"

    def get_album_uid_field(self):
        if self.for_admin:
            return "1024 AS userid"
        return "userid"

    def get_album_sql(self):
        return """
            SELECT id, {}, name, is_shared, start_time, end_time, is_advanced
            FROM {}
        """.format(
            self.get_album_uid_field(), self.get_album_table()
        )

    def get_albums(self):
        self.for_admin = False
        from_dsm_account = self.get_albums_internal()

        self.for_admin = True
        from_admin = self.get_albums_internal()

        albums = from_dsm_account + from_admin
        self.filter_albums(albums)

        return albums

    def filter_albums(self, albums):
        pass

    def get_albums_internal(self):
        self.cursor.execute(self.get_album_sql())

        albums = []
        for (
            id_album,
            uid,
            name,
            is_shared,
            start_time,
            end_time,
            is_advanced,
        ) in self.cursor:
            album = {
                "id": id_album,
                "uid": uid,
                "name": name,
                "shared": is_shared,
                "start_time": self.to_timestamp(start_time),
                "end_time": self.to_timestamp(end_time),
                "is_advanced": is_advanced,
            }

            albums.append(album)

        self.fill_item(albums)
        self.fill_md5ed_password(albums)

        return albums

    def fill_item(self, albums):
        for album in albums:
            id_album = album["id"]
            photos = self.get_photos_by_normal_album_id(id_album)
            videos = self.get_videos_by_normal_album_id(id_album)
            album["item"] = photos + videos

    def fill_md5ed_password(self, albums):
        for album in albums:
            is_advanced = album["is_advanced"]
            id_album = album["id"]

            md5ed_password = ""
            if is_advanced:
                md5ed_password = self.get_password_by_album_id(id_album)
            album["md5ed_password"] = md5ed_password

    def to_timestamp(self, date_object):
        if not date_object:
            return 0
        delta = date_object - datetime.date.fromtimestamp(0)
        return int(delta.total_seconds())

    def get_photos_by_normal_album_id(self, id_album):
        self.cursor.execute(
            """
            SELECT path
            FROM {} as Relation, photo_image as Image
            WHERE
                Relation.collectionid={} AND
                Image.id = Relation.photoid
        """.format(
                self.get_photo_relation_table(), id_album
            )
        )

        items = []
        for [path] in self.cursor:
            items.append(path)

        return items

    def get_videos_by_normal_album_id(self, id_album):
        self.cursor.execute(
            """
            SELECT path
            FROM {} as Relation, video as Video
            WHERE
                Relation.collectionid={} AND
                Video.id = Relation.videoid
        """.format(
                self.get_video_relation_table(), id_album
            )
        )

        items = []
        for [path] in self.cursor:
            items.append(path)

        return items

    def get_password_by_album_id(self, id_album):
        self.cursor.execute(
            """
            SELECT enable_password, password
            FROM {}
            WHERE collectionid = {}
        """.format(
                self.get_advanced_table(), id_album
            )
        )

        row = self.cursor.fetchone()
        if row is None:
            return ""

        enable_password, md5ed_password = row
        if enable_password:
            return md5ed_password
        return ""


class NormalAlbum(BaseAlbum):
    def get_album_sql(self):
        return """
            SELECT id, {}, name, is_shared, start_time, end_time, is_advanced
            FROM {}
            WHERE hidden='f' AND shareid IS NULL
        """.format(
            self.get_album_uid_field(), self.get_album_table()
        )


class SharedAlbums:
    def __init__(self):
        self.cursor = self.init_cursor("photo")
        self.config = self.init_config()
        self.normal_albums = self.init_normal_albums()
        self.apply_share_permission()
        self.uids = self.init_uids()

    def init_cursor(self, db_name):
        user = "postgres"
        unix_sock = "/var/run/postgresql/.s.PGSQL.5432"
        database = db_name
        conn = pg8000.connect(user, unix_sock=unix_sock, database=database)

        cursor = conn.cursor()
        cursor.execute("SET CLIENT_ENCODING TO 'UTF8'")

        return cursor

    def get_config(self):
        return self.config

    def get_normal_albums(self):
        return self.normal_albums

    def get_uids(self):
        return self.uids

    def init_config(self):
        download_config = self.get_download_config()
        sharing_config = self.get_sharing_config()

        return {**download_config, **sharing_config}

    def init_normal_albums(self):
        return NormalAlbum(self.cursor).get_albums()

    def apply_share_permission(self):
        role = "download" if self.config["allow_guest_download"] else "view"

        for album in self.normal_albums:
            album["permission"] = {"role": role, "member": {"type": "public"}}

    def init_uids(self):
        uid_set = set()
        for album in self.normal_albums:
            uid = album["uid"]
            uid_set.add(uid)
        return sorted(list(uid_set))

    def get_download_config(self):
        self.cursor.execute(
            """
            SELECT config_key, config_value
            FROM photo_config
            WHERE config_key IN ({})
        """.format(
                ",".join(["%s"] * len(DOWNLOAD_CONFIG_KEYS))
            ),
            DOWNLOAD_CONFIG_KEYS,
        )

        d = {}
        for key in DOWNLOAD_CONFIG_KEYS:
            d[key] = False
        for key, value in self.cursor:
            d[key] = True if value == "on" else False
        return d

    def get_sharing_privilege(self):
        privilege_map = {
            "0": "administrators",
            "1": "everyone",
            "2": "uids",
        }
        default_privilege = privilege_map["0"]

        self.cursor.execute(
            """
            SELECT config_key, config_value
            FROM photo_config
            WHERE config_key = 'public_sharing_privilege'
        """
        )
        row = self.cursor.fetchone()
        if row is None:
            return default_privilege

        _, privilege_index = row
        return privilege_map.get(privilege_index, default_privilege)

    def get_sharing_uids(self):
        self.cursor.execute(
            """
            SELECT userid
            FROM photo_public_share_right_for_dsm_account
        """
        )

        uid_rows = self.cursor.fetchall()
        return [x for [x] in uid_rows]

    def get_sharing_config(self):
        privilege = self.get_sharing_privilege()

        user_ids = []
        if privilege == "uids":
            user_ids = self.get_sharing_uids()

        return {
            "public_sharing_privilege": privilege,
            "public_sharing_uids": user_ids,
        }


def main():
    shared = SharedAlbums()

    result = {
        "config": shared.get_config(),
        "normal_albums": shared.get_normal_albums(),
        "uids": shared.get_uids(),
    }

    return result


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    result = main()

    if len(sys.argv) >= 2:
        path = sys.argv[1]
        json.dump(result, open(path, "w"))
    else:
        print(json.dumps(result, indent=2))