MealMaster Recipe Extractor

Share your advanced PureBasic knowledge/code with the community.
User avatar
TI-994A
Addict
Addict
Posts: 2512
Joined: Sat Feb 19, 2011 3:47 am
Location: Singapore
Contact:

MealMaster Recipe Extractor

Post by TI-994A »

A threaded version that shouldn't lock up during large conversions. Nonetheless, a work in progress...

Code: Select all

;==============================================================
;   MealMaster Automatic Recipe Extractor
;
;   tested & working with PureBasic v5.70 LTS (x64) running
;   on Windows 8.1, MacOS High Sierra, Linux Ubuntu Bionic
;
;   by TI-994A - free to use, improve, share...
;
;   11th May 2019
;==============================================================

; sample MMF files with different markers available for testing:
; https://www.dropbox.com/s/i87ecbzld6c9rub/apetizer.mmf?dl=1
; https://www.dropbox.com/s/xe9z8n6c8ys1d4n/allrecip.mmf?dl=1

CompilerIf #PB_Compiler_Thread = 1
  
  EnableExplicit
  UseSQLiteDatabase()
  
  Enumeration events #PB_Event_FirstCustomValue   
    #statusBarUpdate
    #importDone
    #exportDone
    #threadQuit
  EndEnumeration
  
  Enumeration 
    #MainWindow 
    #messageWindow
    #messageText
    #statusBar
    #manualMarkers
    #autoDetectMarkers
    #markerTextInput
    #selectMMFFile
    #selectDBFile
    #startProcessing
    #mmfFileRecipes
    #databaseRecipes
    #mmfRecipesLabel
    #dbRecipesLabel
    #mmfFileHandle
    #databaseHandle
    #mmfFileName
    #databaseName
  EndEnumeration
  
  Structure recipeData
    category.s
    title.s
    yield.s 
    ingredients.s
    directions.s
  EndStructure
  
  Declare showWaitWindow()
  Declare processRecipes()
  Declare clearStatusBar()
  Declare.s getFileName(fileType)
  Declare updateStatusBar(postData)
  Declare importMMFRecipes(*mmfFileName)
  Declare exportMMFRecipes(*dbFileName)
  Global NewMap recipes.recipeData()
  Global threadQuit, autoDetectMarkers = #True
  Define.s status, placeholder, mmfFileName, databaseName
  Define appQuit, recipesCount, activeThread
  Define wFlags = #PB_Window_SystemMenu | #PB_Window_ScreenCentered
  
  OpenWindow(#MainWindow, 0, 0, 1030, 700, 
             "Meal-Master Recipe Extractor", wFlags)
  TextGadget(#mmfRecipesLabel, 20, 70, 260, 30, 
             "Recipes extracted from text file:")
  TextGadget(#dbRecipesLabel, 530, 70, 260, 30, 
             "Recipes read from database:")
  OptionGadget(#autoDetectMarkers, 15, 10, 220, 30, "Auto-Detect Recipe Markers")
  OptionGadget(#manualMarkers, 240, 10, 150, 30, "Use This Marker:")
  StringGadget(#markerTextInput, 390, 10, 400, 30, "")
  StringGadget(#mmfFileName, 10, 100, 500, 30, "", #PB_String_ReadOnly)
  StringGadget(#databaseName, 520, 100, 500, 30, "", #PB_String_ReadOnly)
  EditorGadget(#mmfFileRecipes, 10, 135, 500, 530, #PB_Editor_ReadOnly)
  EditorGadget(#databaseRecipes, 520, 135, 500, 530, #PB_Editor_ReadOnly)
  ButtonGadget(#startProcessing, 800, 10, 220, 30, "START PROCESS")
  ButtonGadget(#selectMMFFile, 290, 65, 220, 30, "SELECT MMF FILE")
  ButtonGadget(#selectDBFile, 800, 65, 220, 30, "SELECT/CREATE DB FILE")
  DisableGadget(#markerTextInput, 1)
  SetGadgetState(#autoDetectMarkers, 1)
  
  If CreateStatusBar(#statusBar, WindowID(#MainWindow))
    AddStatusBarField(255)
    AddStatusBarField(255)
    AddStatusBarField(255)
    AddStatusBarField(255)
  EndIf
  
  Repeat
    Select WaitWindowEvent()
      Case #PB_Event_CloseWindow
        If IsThread(activeThread)
          threadQuit = 1
        Else        
          appQuit = 1
        EndIf
      Case #threadQuit
        If threadQuit = 1
          appQuit = 1
        EndIf        
        threadQuit = 0            
        activeThread = 0
        ClearMap(recipes())
      Case #statusBarUpdate
        recipesCount = updateStatusBar(EventData())          
      Case #importDone
        If EventData()        
          activeThread = CreateThread(@exportMMFRecipes(), @databaseName)          
        Else
          ClearMap(recipes())        
          CloseWindow(#messageWindow)
          StatusBarText(#statusBar, 0, "operation failed") 
          SetGadgetText(#startProcessing, "START PROCESS")              
        EndIf      
        StatusBarText(#statusBar, 0, "") 
      Case #exportDone
        activeThread = 0
        ClearMap(recipes())        
        CloseWindow(#messageWindow)
        StatusBarText(#statusBar, 2, "") 
        SetGadgetText(#startProcessing, "START PROCESS")              
      Case #PB_Event_Gadget
        If Not IsThread(activeThread) Or 
           EventGadget() = #startProcessing
          Select EventGadget()
            Case #autoDetectMarkers, #manualMarkers          
              If EventGadget() = #manualMarkers
                autoDetectMarkers = #False
              Else              
                autoDetectMarkers = #True
              EndIf
              DisableGadget(#markerTextInput, autoDetectMarkers)            
            Case #selectMMFFile
              mmfFileName = getFileName(1)
              ClearGadgetItems(#mmfFileRecipes)
              ;placeholder = "Input filename: " + UCase(GetFilePart(mmfFileName))
              SetGadgetText(#mmfFileName, mmfFileName)
              ;AddGadgetItem(#mmfFileRecipes, -1, placeholder)
              ;AddGadgetItem(#mmfFileRecipes, -1, RSet("=", Len(placeholder), "="))
              clearStatusBar()
            Case #selectDBFile
              databaseName = getFileName(2)
              ClearGadgetItems(#databaseRecipes)
              ;placeholder = "Database filename: " + UCase(GetFilePart(databaseName))
              SetGadgetText(#databaseName, databaseName)
              ;AddGadgetItem(#databaseRecipes, -1, placeholder)
              ;AddGadgetItem(#databaseRecipes, -1, RSet("=", Len(placeholder), "="))
              clearStatusBar()
            Case #startProcessing
              If IsThread(activeThread)
                threadQuit = 2
                CloseWindow(#messageWindow)
                SetGadgetText(#startProcessing, "START PROCESS")              
              Else              
                If mmfFileName = "" Or databaseName = ""
                  MessageRequester("Input & Output Files", 
                                   "Please select an MMF file to import " +
                                   #CRLF$ + "and a database file to export to.")
                Else         
                  clearStatusBar()
                  showWaitWindow()              
                  ClearGadgetItems(#mmfFileRecipes)
                  ClearGadgetItems(#databaseRecipes)
                  SetGadgetText(#startProcessing, "STOP PROCESS")
                  activeThread = CreateThread(@importMMFRecipes(), @mmfFileName)  
                EndIf        
              EndIf        
          EndSelect
        EndIf
    EndSelect
  Until appQuit = 1 
  
  Procedure importMMFRecipes(*mmfFileName)
    
    ; =========================
    ;  read & process MMF file
    ; =========================
    
    Protected.s fileData, topMarker, bottomMarker, statMessage
    Protected.s mmfFileName = PeekS(*mmfFileName), success.i = #False
    Protected index, title, category, yield, ingredients, directions, lastPoll
    
    If OpenFile(#mmfFileHandle, mmfFileName)  
      If autoDetectMarkers
        While Not Eof(#mmfFileHandle) And Not threadQuit
          fileData = Trim(ReadString(#mmfFileHandle))
          If Trim(fileData) <> ""
            topMarker = Trim(fileData)
            bottomMarker = Left(topMarker, 5)          
            Break
          EndIf    
        Wend      
      Else
        topMarker = Trim(GetGadgetText(#markerTextInput))
        bottomMarker = Left(topMarker, 5)
      EndIf
      
      PostEvent(#statusBarUpdate, #MainWindow, 0, 0, @"0, 0, importing recipes...")
      
      FileSeek(#mmfFileHandle, 0)  
      While Not Eof(#mmfFileHandle) And Not threadQuit      
        fileData = Trim(ReadString(#mmfFileHandle))      
        If ElapsedMilliseconds() - lastPoll > 100
          statMessage = "0," + Str(index + 1) + ",importing recipe #" + Str(index + 1)
          PostEvent(#statusBarUpdate, #MainWindow, 0, 0, @statMessage)
          lastPoll = ElapsedMilliseconds()
        EndIf      
        If Trim(fileData) <> ""        
          If FindString(fileData, topMarker) > 0        
            title = #True
            Continue        
          ElseIf FindString(fileData, bottomMarker) > 0 And Len(fileData) < 6        
            index + 1   
            directions = #False   
            Continue        
          EndIf                    
          If title 
            fileData = Mid(fileData, FindString(fileData, ":") + 1)
            recipes(Str(index))\title = Trim(fileData)
            title = #False
            category = #True
          ElseIf category
            fileData = Mid(fileData, FindString(fileData, ":") + 1)
            recipes(Str(index))\category = Trim(fileData)
            category = #False
            yield = #True        
          ElseIf yield
            fileData = Mid(fileData, FindString(fileData, ":") + 1)
            recipes(Str(index))\yield = Trim(fileData) 
            ingredients = #True     
            Continue
          ElseIf ingredients
            recipes(Str(index))\ingredients + fileData + #CRLF$
          ElseIf directions
            recipes(Str(index))\directions + fileData + #CRLF$
          EndIf             
        Else        
          If ingredients 
            If yield
              yield = #False
            Else
              ingredients = #False
              directions = #True      
            EndIf        
          EndIf        
        EndIf         
      Wend    
      statMessage = "1," + Str(index) + "," + Str(index) + 
                    " recipes imported successfully"
      CloseFile(#mmfFileHandle)  
      If index > 0
        success = #True
      EndIf    
    Else
      statMessage = "0,0,error opening text file"
    EndIf
    
    If threadQuit
      PostEvent(#threadQuit)
      ProcedureReturn 
    Else
      PostEvent(#statusBarUpdate, #MainWindow, 0, 0, @statMessage)
    EndIf  
    
    ; ============================
    ;  display the extracted data
    ; ============================
    
    If success And Not threadQuit
      index = 1
      ForEach recipes()
        AddGadgetItem(#mmfFileRecipes, -1, recipes()\title)
        AddGadgetItem(#mmfFileRecipes, -1, recipes()\category)
        AddGadgetItem(#mmfFileRecipes, -1, recipes()\yield)
        AddGadgetItem(#mmfFileRecipes, -1, recipes()\ingredients)
        AddGadgetItem(#mmfFileRecipes, -1, recipes()\directions)
        AddGadgetItem(#mmfFileRecipes, -1, "")      
        If ElapsedMilliseconds() - lastPoll > 100
          statMessage = "0," + Str(index) + ",displaying recipe #" + Str(index)
          PostEvent(#statusBarUpdate, #MainWindow, 0, 0, @statMessage)
          lastPoll = ElapsedMilliseconds()
        EndIf
        index + 1           
        If threadQuit : Break : EndIf  
      Next     
    EndIf
    
    If threadQuit
      PostEvent(#threadQuit)
    Else
      PostEvent(#importDone, #MainWindow, 0, 0, success)
    EndIf    
    
  EndProcedure
  
  Procedure exportMMFRecipes(*dbFileName)
    
    ; ============================
    ;  create & write to database
    ; ============================
    
    Protected.s insertQuery, statMessage, query
    Protected.s dbFileName = PeekS(*dbFileName)  
    Protected result = #True, index = 1, i, lastPoll
    
    If CreateFile(#mmfFileHandle, dbFileName)
      CloseFile(#mmfFileHandle)      
      If OpenDatabase(#databaseHandle, dbFileName, "", "")         
        If DatabaseUpdate(#databaseHandle, "CREATE TABLE recipes (" +
                                           "id INTEGER PRIMARY KEY," +
                                           "recipe CHAR(100), " +
                                           "scat CHAR(50), " +                         
                                           "serves CHAR(50)," +
                                           "ingredients CHAR(500), " +
                                           "directions CHAR(500))")          
          insertQuery = "INSERT INTO " +
                        "recipes (recipe, scat, serves, ingredients, directions) " +
                        "VALUES (?, ?, ?, ?, ?)"             
          PostEvent(#statusBarUpdate, #MainWindow, 0, 0, @"2, 0, exporting recipes...")
          ForEach recipes()
            With recipes()
              SetDatabaseString(#databaseHandle, 0, \title)
              SetDatabaseString(#databaseHandle, 1, \category)          
              SetDatabaseString(#databaseHandle, 2, \yield)
              SetDatabaseString(#databaseHandle, 3, \ingredients)
              SetDatabaseString(#databaseHandle, 4, \directions)            
              If DatabaseUpdate(#databaseHandle, insertQuery)              
                If ElapsedMilliseconds() - lastPoll > 100
                  statMessage = "2," + Str(index + 1) + ",exporting recipe #" + 
                                Str(index + 1) + " to database"
                  PostEvent(#statusBarUpdate, #MainWindow, 0, 0, @statMessage)
                  lastPoll = ElapsedMilliseconds()
                EndIf              
                index + 1
              Else            
                statMessage = "2,0,error inserting data"
                result = #False
                Break
              EndIf                 
            EndWith          
            If threadQuit : Break : EndIf                    
          Next                
        Else 
          statMessage = "2,0,error creating table"
        EndIf 
        CloseDatabase(#databaseHandle)
      Else    
        statMessage = "2,0,error opening database"
      EndIf
    Else    
      statMessage = "2,0,error creating database file"
    EndIf
    
    If result
      statMessage = "3," + Str(index -1) + "," + Str(index - 1) + 
                    " recipes written to database"
    EndIf
    
    If threadQuit 
      PostEvent(#threadQuit) 
      ProcedureReturn 
    Else    
      PostEvent(#statusBarUpdate, #MainWindow, 0, 0, @statMessage)  
    EndIf
    
    ; ==========================
    ;  read & display database
    ; ==========================
    
    If OpenDatabase(#databaseHandle, dbFileName, "", "") And Not threadQuit
      query = "SELECT * FROM recipes"
      If DatabaseQuery(#databaseHandle, query)
        index = 1
        While NextDatabaseRow(#databaseHandle) And Not threadQuit
          If ElapsedMilliseconds() - lastPoll > 100
            statMessage = "2," + Str(index) + ",displaying recipe #" + 
                          Str(index) + " from database"
            PostEvent(#statusBarUpdate, #MainWindow, 0, 0, @statMessage)
            lastPoll = ElapsedMilliseconds()
          EndIf 
          For i = 1 To 5
            AddGadgetItem(#databaseRecipes, -1, GetDatabaseString(#databaseHandle, i))           
          Next i        
          AddGadgetItem(#databaseRecipes, -1, "")
          index + 1        
        Wend                        
        FinishDatabaseQuery(#databaseHandle)                    
      EndIf
      CloseDatabase(#databaseHandle)
    EndIf  
    
    If threadQuit 
      PostEvent(#threadQuit) 
    Else
      PostEvent(#exportDone)
    EndIf
    
  EndProcedure
  
  Procedure.s getFileName(fileType)
    Protected.s pattern, title
    If fileType = 1
      title = "Select MMF input file:"
      pattern = "MealMaster Files (*.mmf)|*.mmf|Text Files (*.txt)|*.txt"  
    Else
      title = "Select Database output file:"
      pattern = "SQL Files (*.sql, *.sqlite)|*.sql;*.sqlite|Database Files (*.db)|*.db"  
    EndIf  
    ProcedureReturn OpenFileRequester(title, "C\", pattern, 0)
  EndProcedure
  
  Procedure showWaitWindow()
    Protected statMessage.s
    Protected wFlags = #PB_Window_ScreenCentered | #PB_Window_BorderLess
    Protected waitMessage.s = "Processing recipes." + #CRLF$ + "Please wait..."
    OpenWindow(#messageWindow, 0, 0, 200, 100, "", wFlags, WindowID(#MainWindow))  
    TextGadget(#messageText, 0, 30, 200, 40, waitMessage, #PB_Text_Center)    
  EndProcedure
  
  Procedure updateStatusBar(postData)
    Protected statIndex, recipesCount, statAlign
    Protected.s statusData, statMessage
    statusData = PeekS(postData)
    statIndex = Val(StringField(statusData, 1, ","))
    recipesCount = Val(StringField(statusData, 2, ","))  
    statMessage = StringField(statusData, 3, ",") 
    If statIndex = 1 Or statIndex = 3
      statAlign = #PB_StatusBar_Right
    EndIf  
    StatusBarText(#statusBar, statIndex, statMessage, statAlign)  
    ProcedureReturn recipesCount
  EndProcedure
  
  Procedure clearStatusBar()
    Protected i
    For i = 0 To 3
      StatusBarText(#statusBar, i, "") 
    Next i  
  EndProcedure
  
CompilerElse
  
  MessageRequester("Compiler", "Please enable threadsafe option in compiler options.")
  
CompilerEndIf
As always, suggestions, feedback, and improvements are most welcome. Thank you. :D
Texas Instruments TI-99/4A Home Computer: the first home computer with a 16bit processor, crammed into an 8bit architecture. Great hardware - Poor design - Wonderful BASIC engine. And it could talk too! Please visit my YouTube Channel :D