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