├── requirements.txt ├── LICENSE ├── README.md └── gp.py /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | cachetools==3.1.0 3 | certifi==2019.3.9 4 | cffi==1.12.2 5 | chardet ==3.0.4 6 | cryptography==3.3.2 7 | entrypoints==0.3 8 | google-api-python-client==1.7.8 9 | google-auth==1.6.3 10 | google-auth-httplib2==0.0.3 11 | google-auth-oauthlib==0.2.0 12 | httplib2==0.19.0 13 | idna==2.8 14 | jeepney==0.4 15 | keyring==18.0.0 16 | oauth2client==4.1.3 17 | oauthlib==3.0.1 18 | pyasn1==0.4.5 19 | pyasn1-modules==0.2.4 20 | pycparser==2.19 21 | requests==2.26.0 22 | requests-oauthlib==1.2.0 23 | rsa==4.7 24 | SecretStorage==3.1.1 25 | six==1.12.0 26 | uritemplate==3.0.0 27 | urllib3 ==1.26.5 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 gustible 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # googlephotos bulk uploader 2 | Bulk uploads photos from a local folder to Google Photos. Uses a specified album to categorise the photos. 3 | Progress of upload is stored locally. 4 | Meant for uploading large numbers of files - personally used it to upload more than 60,000 photos 5 | to Google Drive. 6 | 7 | --- 8 | ## Deployment 9 | This is a command line tool. 10 | Download and create a virtual environment: 11 | 12 | mkvirtualenv -p python3 googlephotos (_or any other name_) 13 | pip install -r requirements.txt 14 | python ./gp.py -? 15 | 16 | *Note: On on Windows use Python 3.8 version max to avoid legacy-install-failure error while trying to install package `cffi`* 17 | 18 | ## Configuration 19 | To avoid having to re-authenticate with Google all the time, the application expects credentials to be 20 | available in a local file. The default file name is _credentials.json_ 21 | 22 | The method for obtaining the credentials are described here - https://www.syncwithtech.org/authorizing-google-apis/ 23 | Run the script with option -gc to obtain new OAuth credentials via the command line. 24 | 25 | Folders (image directory) are set to default values but can be overridden on the command line. 26 | The root folder for media (IMAGEDIR) is NOT saved in the database, but only read from the command line. This means that 27 | if you want to change the root folder you can do so via configuration only. Folder below the root ARE stored in the 28 | database. For example: if the root folder is "PICTURES", and you add an image with path "PICTURES/October/100.JPG" then 29 | the image's local path will be stored as "October/100.JPG" in the database. The combination of IMAGEDIR and localpath is 30 | used to locate files that will be uploaded. 31 | 32 | ## Credentials 33 | Follow those steps to download the credentials required: 34 | 35 | 1. Create project on google cloud 36 | 2. Enable Google Photos Library API for this project: go to https://developers.google.com/photos/library/guides/get-started then click button "Enable the Google Photos Library API" 37 | 3. For "Configure OAuth client" screen select ~~"Installed Application"~~ "Desktop app" 38 | 4. Download client configuration and place this file `credentials.json` at project root (optional: store client_id and client_secret somewhere safe) 39 | 5. Run `python ./gp.py --get_credentials`, open link in web browser then paste Authorization code back in console 40 | 41 | ## Usage 42 | usage: gp.py [-h] [-i IMAGEDIR] [-a ALBUMNAME] [-x] [-c] [-u] [-v] [-m MAXSIZE] 43 | 44 | -h, --help show this help message and exit 45 | 46 | -gc, --get_credentials 47 | Obtains new OAuth credentials and saves them. Other 48 | parameters are ignored. 49 | 50 | -i IMAGEDIR, --imagedir IMAGEDIR 51 | Specify root image directory 52 | 53 | -a ALBUMNAME, --albumname ALBUMNAME 54 | Specify Google Photos album 55 | 56 | -x, --dontincrementalbum 57 | Auto increment album name for large albums 58 | 59 | -c, --check Walk local folder and update database of files, mark 60 | new files for upload. Files are NOT uploaded unless -u 61 | is True. The folder is first walked, then uploaded. 62 | 63 | -u, --upload Upload images to Google Drive 64 | 65 | -v, --verbose Provide verbose messaging 66 | 67 | -m MAXSIZE, --maxsize MAXSIZE 68 | Max file size to upload (MB), default=-1 (no limit) 69 | 70 | __Example:__ 71 | 72 | Basic usage patter is as follows: 73 | - Run the script to create a list of all files to be uploaded (/.gp.py -v) 74 | - Run the script to upload each file in the database (.gp.py -u) 75 | 76 | To upload all JPG files in the default image folder that are small 77 | than 10MB, and with verbose output, use: 78 | 79 | python ./gp.py -u -v -m 10 80 | 81 | To update the database of all files that must be uploaded, showing verbose 82 | messages use: 83 | 84 | python ./gp.py -c -v 85 | 86 | 87 | __Notes:__ 88 | - The script will ONLY upload files with a JPG extension (it is not case sensitive). 89 | - The script can easily be modified to accommodate other files, the JPG limitation is just a personal requirement 90 | - The SQLite database is called GDriveimages, and is created in the folder where the script is located 91 | 92 | 93 | --- 94 | 95 | -------------------------------------------------------------------------------- /gp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, sqlite3, json, sys, time 4 | 5 | from inspect import getsourcefile 6 | from datetime import datetime 7 | from google.auth.transport.requests import AuthorizedSession 8 | from google_auth_oauthlib.flow import InstalledAppFlow 9 | from google.oauth2.credentials import Credentials 10 | import argparse 11 | 12 | 13 | SCOPES = [ 14 | 'https://www.googleapis.com/auth/photoslibrary', 15 | 'https://www.googleapis.com/auth/photoslibrary.sharing' 16 | ] 17 | CLIENT_SECRET_FILE = 'credentials.json' 18 | API_SERVICE_NAME = 'photoslibrary.googleapis.com' 19 | API_VERSION = 'v1' 20 | # DATABASE_PATH = '/home/jacques/workspace/googlephotos/GDriveimages' 21 | DATABASE_NAME= 'GDriveimages' 22 | 23 | # region Custom Exceptions 24 | 25 | 26 | class CriticalError(Exception): 27 | pass 28 | 29 | 30 | class IgnoreFileError(Exception): 31 | pass 32 | 33 | 34 | class AlbumFullError(Exception): 35 | pass 36 | 37 | 38 | class UploadError(Exception): 39 | pass 40 | 41 | # endregion Custom Exceptions 42 | 43 | 44 | def print_message(message): 45 | if args.verbose: 46 | print(message) 47 | 48 | 49 | def create_database_structure(): 50 | """ 51 | Sets up the database structure only if the tables do not exist 52 | :return: None 53 | """ 54 | sql = "create table if not exists imagelist(localpath, google_id, dateuploaded)" 55 | connsql.execute(sql) 56 | sql = "create table if not exists albumname(album_name, increment, google_id)" 57 | connsql.execute(sql) 58 | sql = "create table if not exists token(token, refresh_token, token_uri, " \ 59 | "client_id, client_secret, datesaved)" 60 | connsql.execute(sql) 61 | 62 | 63 | def create_album(album_name, authed_session): 64 | """ 65 | Creates the album in Google Photos 66 | :param album_name: Name of the album to create 67 | :param authed_session: AuthorizedSession object 68 | :return: album_id - the Google ID for the album 69 | """ 70 | url = 'https://photoslibrary.googleapis.com/v1/albums' 71 | payload = { 72 | "album": { 73 | "title": album_name 74 | } 75 | } 76 | try: 77 | response = authed_session.post(url, data=json.dumps(payload)) 78 | except Exception as e: 79 | raise CriticalError(e) 80 | if response.status_code != 200: 81 | raise CriticalError("Could not create album:{}".format(response.text)) 82 | return json.loads(response.text)["id"] 83 | 84 | 85 | def get_active_album_name(increment=0): 86 | """ 87 | Returns the name of the current album, optionally with an increment. This is used if the Google Album size limit is reached. 88 | The album name is automatically incremented by 1 every time the size is exceeded. 89 | The active album name is the album name specified in the parameters + _{increment}. 90 | Note - only one record is expected in this table. Ever. 91 | :return: albumname and google_id for the album 92 | """ 93 | c = connsql.cursor() 94 | c.execute('select album_name, increment, google_id from albumname') 95 | album = c.fetchone() 96 | 97 | if album is None: 98 | return args.albumname, None 99 | if album[1] > 0: 100 | return args.albumname + "_{}".format(album[1]+increment), album[2] 101 | else: 102 | return args.albumname, album[2] 103 | 104 | 105 | def increment_album_name(authed_session): 106 | """ 107 | Adds an increment to the album name if the count of items exceed 20000. 108 | Creates the new album on Google Photos 109 | :param authed_session: 110 | :return: album_id, album_name 111 | """ 112 | 113 | album_name, album_id = get_active_album_name(1) 114 | album_id = create_album(album_name, authed_session) 115 | try: 116 | c = connsql.cursor() 117 | sql = "UPDATE albumname set increment = increment+1, google_id = '{}'".format(album_id) 118 | c.execute(sql) 119 | connsql.commit() 120 | except Exception as e: 121 | raise CriticalError(e) 122 | print_message("Incremented album - new album is:{}".format(album_name)) 123 | return album_id, album_name 124 | 125 | 126 | def check_album_item_count(authed_session, album_id): 127 | url = "https://photoslibrary.googleapis.com/v1/albums/{}".format(album_id) 128 | response = authed_session.get(url) 129 | if response.status_code != 200: 130 | raise CriticalError("Unable to retrieve album list:{}".format(response.text)) 131 | result = json.loads(response.text) 132 | return int(result["mediaItemsCount"]) 133 | 134 | 135 | def set_up_album(authed_session): 136 | """ 137 | Sets up the album - either creates it or creates a new album when the mediacount is over 20000 138 | is "incremented" when the media count exceeds 20000 (the limit imposed by Google) 139 | This will first check the album size - if it is large than 20000 it will be incremented. 140 | The process tracks the file count and will increment as needed 141 | :param authed_session: 142 | :return: album_id, album_name 143 | """ 144 | 145 | album_name, album_id = get_active_album_name() 146 | album_count = 0 147 | if not album_id: 148 | # Album was not found, create it. 149 | album_id = create_album(album_name, authed_session) 150 | try: 151 | c = connsql.cursor() 152 | sql_parameters = (args.albumname, 0, album_id) 153 | c.execute("INSERT INTO albumname (album_name, increment, google_id) VALUES (?,?,?)", sql_parameters) 154 | connsql.commit() 155 | except Exception as e: 156 | raise CriticalError(e) 157 | else: 158 | album_count = check_album_item_count(authed_session, album_id) 159 | if album_count >= 20000: 160 | album_id, album_name = increment_album_name(authed_session) 161 | album_count = 0 162 | print_message("Using album:{}".format(album_name)) 163 | 164 | return album_id, album_name, album_count 165 | 166 | 167 | def set_file_status(rowid, google_id): 168 | c = connsql.cursor() 169 | sql_parameters = (google_id, datetime.now().isoformat(), rowid) 170 | c.execute("UPDATE imagelist set google_id = ?, dateuploaded = ? where rowid = ?", sql_parameters) 171 | connsql.commit() 172 | 173 | 174 | def store_file_details(localpath): 175 | """ 176 | Simply store the file's path in the database - GoogleID is defaulted to None 177 | Performs a check to see if the file is already in the database, if it is, do not add it again 178 | :param localpath: full local path to the file 179 | :param google_id: the google ID of the file if available 180 | :param uploaded: TRUE if it has been successfully uploaded 181 | :return: None 182 | """ 183 | exists = False 184 | c = connsql.cursor() 185 | parms = (localpath,) 186 | c.execute('select rowid from imagelist where localpath = ?;', parms) 187 | exists = c.fetchone() is not None 188 | 189 | if not exists: # Do not add to database if google ID exists or filepath exists 190 | print_message("Adding:{0}".format(localpath)) 191 | sql_parameters = (localpath, None, None) 192 | c.execute("INSERT INTO imagelist (localpath, google_id, dateuploaded) VALUES (?,?,?)", sql_parameters) 193 | connsql.commit() 194 | return 195 | print_message("File already in list - name:{}".format(localpath)) 196 | 197 | 198 | def check_files(local_dir): 199 | """ 200 | Walks the local directory and adds files which are not registered in the database. Does NOT upload 201 | files. Only check for JPG (jpg) files. 202 | TODO: Add command line argument to check for other types 203 | :param local_dir: The local folder to check for files which have not been uploaded 204 | :return: None 205 | """ 206 | print_message("::Walking local folder") 207 | for (dirpath, dirnames, filenames) in os.walk(local_dir): 208 | for f in filenames: 209 | ext = os.path.splitext(f)[1].upper() 210 | if ext in [".JPG"]: 211 | filepath = os.path.join(dirpath, f) 212 | if local_dir[-1] != '/': 213 | f_path = filepath.replace("{}/".format(local_dir), '', 1) 214 | else: 215 | f_path = filepath.replace(local_dir, '', 1) 216 | print(f_path) # Remove the path to the image dir 217 | store_file_details(f_path) 218 | 219 | 220 | def get_authed_session(): 221 | """ 222 | Returns an AuthorizedSession object based on stored credentials 223 | :return: sAuthorizedSession 224 | """ 225 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' 226 | credentials = read_credentials() 227 | # credentials = None 228 | if credentials is None: 229 | credentials = get_oauth_credentials() 230 | store_token(credentials) 231 | return AuthorizedSession(credentials) 232 | 233 | 234 | def upload_file(authed_session, album_id, file_name): 235 | """ 236 | 237 | :param authed_session: 238 | :param album_id: 239 | :param file_name: 240 | :return: True if saved OK, new item id, and album id (which may have been modified) 241 | """ 242 | 243 | # Upload the file 244 | f = open(file_name,'rb').read() 245 | if len(f) == 0: 246 | raise IgnoreFileError("This file is zero length:{}".format(file_name)) 247 | headers = { 248 | 'Content-type': 'application/octet-stream', 249 | 'X-Goog-Upload-File-Name': os.path.basename(file_name), 250 | 'X-Goog-Upload-Protocol': 'raw' 251 | } 252 | url = "https://photoslibrary.googleapis.com/v1/uploads" 253 | response = authed_session.post(url, headers=headers, data=f) 254 | if response.status_code != 200: 255 | raise IgnoreFileError("Upload failed:{}".format(response.text)) 256 | # Add it to an album 257 | url = "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate" 258 | upload_token = str(response.content, 'utf-8') 259 | new_media_item = { 260 | "description": os.path.basename(file_name), 261 | "simpleMediaItem": { 262 | "uploadToken": upload_token 263 | } 264 | } 265 | payload = { 266 | "albumId": album_id, 267 | "newMediaItems": [new_media_item] 268 | } 269 | response = authed_session.post(url, data=json.dumps(payload)) 270 | new_media_item_results = json.loads(response.content)['newMediaItemResults'][0] 271 | 272 | if new_media_item_results["status"]["message"] == "Success": 273 | item_id = new_media_item_results["mediaItem"]["id"] 274 | elif new_media_item_results["status"]["code"] == 8: #8 means ERR_PHOTO_PER_ALBUM_LIMIT 275 | raise AlbumFullError("Album Full") 276 | else: 277 | item_id = None 278 | raise UploadError("Unhandled status error:{}".format(new_media_item_results["status"])) 279 | return new_media_item_results["status"]["message"] =="Success", item_id, album_id 280 | 281 | 282 | def upload_files(authed_session, album_id, album_name, album_count): 283 | """ 284 | Uploads all files in the database that are marked as having no Google ID 285 | :return: 286 | """ 287 | c = connsql.cursor() 288 | c.execute("SELECT rowid,* from imagelist where google_id is null") 289 | l = c.fetchall() 290 | 291 | count = 0 292 | print("{0} Files to be uploaded\n".format(len(l))) 293 | print("::Uploading files\n") 294 | for row in l: 295 | try: 296 | file_name = "{}/{}".format(args.imagedir.rstrip('/'), row[1]) 297 | smb = os.path.getsize(file_name) 298 | if (args.maxsize != -1) and (smb // 1048576) > args.maxsize: 299 | print_message("Skipping large file: {0} - size:{1} (max={2}MB)".format(file_name, smb // 1048576, args.maxsize)) 300 | else: 301 | print_message("uploading:{0}, {1} to \'{2}\'".format(row[0], file_name, album_name)) 302 | success, id, album_id = upload_file(authed_session, album_id, file_name) 303 | if success: 304 | set_file_status(row[0], id) 305 | count += 1 306 | print_message("({0}/{1}) uploaded:: {2}".format(count, len(l), file_name)) 307 | if album_count + count >= 20000: 308 | raise AlbumFullError() 309 | else: 310 | print_message("Failed to upload {}".format(file_name)) 311 | except AlbumFullError as e: 312 | print("Album full - incrementing album name NOW") 313 | if args.dontincrementalbum: 314 | raise CriticalError("Album limit reached but dontincrementalbum flag is set. Cannot continue upload") 315 | album_id, album_name = increment_album_name(authed_session) 316 | except IgnoreFileError as e: 317 | print("Ignoring file {0} with error: \'{1}\'".format(file_name, e)) 318 | except OSError as e: 319 | print("Skipping: OSError ignored for file: {0}. Error:{1}".format(file_name, e)) 320 | except UploadError as e: 321 | raise e 322 | 323 | 324 | def list_albums(authed_session): 325 | url = "https://photoslibrary.googleapis.com/v1/albums" 326 | response = authed_session.get(url) 327 | if response.status_code != 200: 328 | raise CriticalError("Unable to retrieve album list:{}".format(response.text)) 329 | result = json.loads(response.text) 330 | albums = result["albums"] 331 | for a in albums: 332 | print("title:{}".format(a["title"])) 333 | print("mediaItemsCount:{}".format(a["mediaItemsCount"])) 334 | print("id:{}".format(a["id"])) 335 | 336 | 337 | # region Token Management 338 | def get_oauth_credentials(): 339 | """ 340 | Obtain an access token using the install app flow. 341 | Reads the client secret from a local file 342 | :return: Credentials object 343 | """ 344 | flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES) 345 | credentials = flow.run_console() 346 | return credentials 347 | 348 | 349 | def store_token(credentials): 350 | """ 351 | Store credentials. Will only ever store a single row. 352 | :param credentials: credentials including access token to store 353 | :return: - 354 | """ 355 | c = connsql.cursor() 356 | 357 | print_message("Storing token:{0}".format(credentials.token)) 358 | c.execute("DELETE FROM token") 359 | sql_parameters = (credentials.token, credentials.refresh_token, 360 | credentials.token_uri, credentials.client_id, 361 | credentials.client_secret, datetime.now().isoformat()) 362 | c.execute("INSERT INTO token (token, refresh_token, token_uri, " \ 363 | "client_id, client_secret, datesaved) VALUES (?,?,?,?,?,?)", sql_parameters) 364 | connsql.commit() 365 | 366 | 367 | def read_credentials(): 368 | """ 369 | Read stored credentials from the database 370 | See: https://www.syncwithtech.org/authorizing-google-apis/ 371 | :return: Credentials object or None 372 | """ 373 | c = connsql.cursor() 374 | c.execute("SELECT * FROM token") 375 | l = c.fetchone() 376 | if l is None: 377 | return None 378 | token = l[0] 379 | refresh_token = l[1] 380 | token_uri = l[2] 381 | client_id = l[3] 382 | client_secret = l[4] 383 | credentials = Credentials(token=token, refresh_token=refresh_token, 384 | token_uri=token_uri, client_id=client_id, 385 | client_secret=client_secret, scopes=SCOPES) 386 | return credentials 387 | # endregion Token Management 388 | 389 | 390 | def main(): 391 | 392 | if args.get_credentials: 393 | get_oauth_credentials() 394 | exit() 395 | 396 | if args.listalbums: 397 | authed_session = get_authed_session() 398 | list_albums(authed_session) 399 | 400 | if args.check: 401 | create_database_structure() 402 | check_files(args.imagedir) 403 | 404 | if args.upload: 405 | create_database_structure() 406 | authed_session = get_authed_session() 407 | album_id, album_name, album_count = set_up_album(authed_session) 408 | 409 | rcount = 0 410 | while True: 411 | try: 412 | upload_files(authed_session, album_id, album_name, album_count) 413 | except CriticalError as e: 414 | print("Critical Exception occurred: {0}".format(e)) 415 | print(">>> Sorry - that was a critical error. Please sort it out and try again") 416 | break 417 | except Exception as e: 418 | # Simple retry - MAY resolve transient connectivity issue. 419 | rcount += 1 420 | print("Exception occurred: {0}".format(e)) 421 | print ("Retry #{0} of 10 in {1} second(s)".format(rcount, rcount*2)) 422 | time.sleep(rcount*2) 423 | if rcount > 9: 424 | print(">>> Sorry - it is not working. Check exceptions and restart manually") 425 | raise 426 | else: 427 | continue 428 | break 429 | 430 | 431 | # region Command line options 432 | 433 | parser = argparse.ArgumentParser(description="Command line upload to GooglePhotos") 434 | 435 | 436 | parser.add_argument("-gc", "--get_credentials", action="store_true", help="Obtains new OAuth credentials and saves them. Other parameters are ignored.") 437 | parser.add_argument("-i", "--imagedir", default="/media/jvn/FILESTORE/Pictures", help="Specify root image directory") 438 | parser.add_argument("-a", "--albumname", default="Backup from Local", help="Specify Google Photos album") 439 | parser.add_argument("-x", "--dontincrementalbum", action="store_true", help="Auto increment album name for large albums") 440 | parser.add_argument("-c", "--check", action="store_true", 441 | help="Walk local folder and update database of files, mark new files for upload. \ 442 | Files are NOT uploaded unless -u is True. The folder is first walked, then uploaded.") 443 | parser.add_argument("-u", "--upload", action="store_true", help="Upload images to Google Drive") 444 | parser.add_argument("-v", "--verbose", action="store_true", help="Provide verbose messaging") 445 | parser.add_argument("-m", "--maxsize", type=int, default=-1, 446 | help="Max file size to upload (MB), default=-1 (no limit)") 447 | parser.add_argument("--listalbums", action="store_true", help="List all albums and exit") 448 | 449 | args = parser.parse_args() 450 | 451 | # endregion 452 | 453 | if __name__ == '__main__': 454 | print("\nRunning with options:") 455 | if args.listalbums: 456 | print("Listing albums and exiting") 457 | else: 458 | print("Imagedir :{}".format(args.imagedir)) 459 | print("Album Name :{}".format(args.albumname)) 460 | print("Increment Album :{}".format(args.dontincrementalbum)) 461 | print("Upload :{}".format(args.upload)) 462 | print("Check :{}".format(args.check)) 463 | print("MaxSize (MB) :{}".format(args.maxsize)) 464 | print("Verbose :{}".format(args.verbose)) 465 | 466 | try: 467 | db_path = "{}/{}".format(os.path.dirname(getsourcefile(lambda:0)), DATABASE_NAME) 468 | connsql = sqlite3.connect(db_path) 469 | main() 470 | 471 | except Exception: 472 | raise 473 | finally: 474 | connsql.close() 475 | print("\n::Done::") 476 | --------------------------------------------------------------------------------