genanki icon indicating copy to clipboard operation
genanki copied to clipboard

How to import media file from memory instead of from hdd?

Open dragancevs opened this issue 3 years ago • 4 comments

Hello, is there a way how to insert media file from memory instead of hdd? I want copy every image to the memory and then insert it to the card. Now i’m importing many images to the cards, the problem is that i must rename every image before i can insert it to the card. Or is there another better way? I know i can create renamed copy of every image but it would unnecessarily strains the hdd.

dragancevs avatar Jan 29 '22 23:01 dragancevs

Easy. Just import genanki then create a new package class that inherits from genanki's package class. Then simply override the requisite write_to_file function to a custom one that writes media files from either memory or db or whatever by switching out the outzip.write to a outzip.writestr with a bytes array of the media data you want to write.

yash-fn avatar Feb 03 '22 22:02 yash-fn

Here is something off the cuff totally untested but should give you something to start with: You then must specify a media_function with three args (outzip, path, idx) that will use outzip.writestr to write whatever you want to resultant apkg/zip file.

from genanki import package
from typing import Optional
import zipfile
from copy import deepcopy
import os
import json

class Package(package.Package):
    def __init__(self, deck_or_decks=None, media_files=None, media_function=None):
        super().__init__(deck_or_decks, media_files)
        self.media_function = media_function
    
    def write_to_file(self, file, timestamp: Optional[float] = None):
        if callable(self.media_function):
            media_files = deepcopy(self.media_files)
            self.media_files = []
            ret = super().write_to_file(file, timestamp)
            self.media_files = media_files
            with zipfile.ZipFile(file, 'a') as outzip:
              media_file_idx_to_path = dict(enumerate(self.media_files))
              media_json = {idx: os.path.basename(path) for idx, path in media_file_idx_to_path.items()}
              outzip.writestr('media', json.dumps(media_json))
              for idx, path in media_file_idx_to_path.items():
                self.media_function(outzip, path, idx)
            return ret
        else:
            return super().write_to_file(file, timestamp)

yash-fn avatar Feb 03 '22 22:02 yash-fn

Thank you very much for your help. I will try it.

dragancevs avatar Feb 04 '22 00:02 dragancevs

Hey, so don't use what I previously wrote. it will leave duplicate media files (not sure if this can cause errors, but just to be safe here is alternative). Use this instead the following. When tested on my own decks it worked fine. Also below that is an example of a media_function that I used to get values from db. Notice how genanki only really needs idx and data for said idx, while path really can be whatever you want it to be in collection.media anki folder. Hence use path to be unique identifier for you to identify the correct asset to write in media function.

from genanki import package
from typing import Optional
import zipfile
from copy import deepcopy
import os
import json

class Package(package.Package):
  def __init__(self, deck_or_decks=None, media_files=None, media_function=None):
      super().__init__(deck_or_decks, media_files)
      self.media_function = media_function
    
  def write_to_file(self, file, timestamp: Optional[float] = None):
    with tempfile.NamedTemporaryFile() as dbfilename:

      with sqlite3.connect(dbfilename.name) as conn:
        cursor = conn.cursor()
        if timestamp is None: timestamp = time.time()
        id_gen = itertools.count(int(timestamp * 1000))
        self.write_to_db(cursor, timestamp, id_gen)

      with tempfile.NamedTemporaryFile(dir=os.path.dirname(file), suffix='.apkg', delete=False) as tempapkg:
        with zipfile.ZipFile(tempapkg.name, 'w') as outzip:
          outzip.write(dbfilename.name, 'collection.anki2')

          media_file_idx_to_path = dict(enumerate(self.media_files))
          media_json = {idx: os.path.basename(path) for idx, path in media_file_idx_to_path.items()}
          outzip.writestr('media', json.dumps(media_json))

          for idx, path in media_file_idx_to_path.items():
            if callable(self.media_function):
              self.media_function(outzip, idx, path)
            else:
              outzip.write(path, str(idx))
        try:
          shutil.move(tempapkg.name, file)
        except Exception as e:
          tempapkg.close()
          raise e
    def media_function(outzip, idx, path): 
        hash_id, table = media_files[path]
        data = con.execute(f'select data from {table} where hash_id = ?', (hash_id,)).fetchone()[0]
        outzip.writestr(str(idx), data)

yash-fn avatar Feb 08 '22 22:02 yash-fn